Settle the machine-name disposition and enumerate what stays

6af97278d163 · AtlantisPleb · · parent ddfdccb42c61

Settle the machine-name disposition and enumerate what stays

`docs/taxonomy.md` carried a provisional list of `machine` surfaces and said
issue #134 would settle whether they are renamed, kept, or split. This settles
it as a split, with one rule that decides every case:

  A `machine` name stays where something outside this release can observe or
  replay it. Everywhere else it becomes `computer`.

"Outside this release" has four forms — a row an earlier release wrote, a
client this repository does not ship, another node during a hot upgrade, and a
sealed ciphertext — and every retained name is one of them. A name that is
none of them lives only in this release's source and moves; that is what
`machine:status` and the `policy_facets` consent did, and what the retired
`sarah.machine_token.v1` AAD failed to justify.

The durable half is now a ledger rather than a paragraph.
`OpenAgents.Vocabulary` enumerates three tables, five columns, nineteen
constraints, and thirteen indexes, and `OpenAgents.VocabularyTest` derives the
live set from `information_schema` and `pg_catalog` instead of from a list
maintained beside it. A migration cannot add a `machine`-named durable surface
without recording it, and a ledger entry the database does not have fails too.

The wire half has no query that enumerates it, so it stays a list in
`docs/taxonomy.md` — deliberately a narrower claim than the durable half, held
by the contracts that own each surface. That list now records what the previous
one omitted: the Horde registry key and cluster messages, the persisted
incident codes that `Incidents.Triage` classifies from stored rows, the
`audit_events.actor_type` value that a computer's Git push writes, the module
residency `operator_machine` and routing-policy id
`sarah.routing.policy.paired-machine.v1` that sit inside a stored
`module_route_receipts` digest, `counts.machines_connected` as an exact key
under STATUS-001, and `OPENAGENTS_MACHINE_TOKEN_TTL_SECONDS`.

It also records a word that is not residue: `contribution.machine` and
`representations.machine` mean machine-readable, pair with `human`, and name a
format rather than a device. A future rename pass should leave them alone.

Three findings that are not naming questions are filed rather than folded in:
#182, #183, and #184.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTmy4SEXrHXouw5sZbs3f4
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified INVARIANTS.md
  • modified docs/taxonomy.md
  • added lib/openagents/vocabulary.ex
  • added test/openagents/vocabulary_test.exs

Diff

4 files changed, +240 -9

INVARIANTS.md modified +37

@@ -44,6 +44,42 @@ Evidence: `OpenAgents.Persona.SourceManifest`,

44 44
`priv/sarah/persona/sarah.v1.sources.json`, and
45 45
`OpenAgents.Persona.SourceManifestTest`.
46 46
47
### CANON-002 — Retained `machine` names are enumerated, not assumed
48
49
Status: Current
50
51
The product says computer. A `machine` name is retained only where something
52
outside this release can observe or replay it: a row an earlier release wrote,
53
a client this repository does not ship, another node during a hot upgrade, or a
54
sealed ciphertext. Everywhere else the name is `computer`.
55
56
The durable half of that exemption is enumerated, not described.
57
`OpenAgents.Vocabulary` lists every `machine`-named table, column, constraint,
58
and index, and the proof derives the live population from `information_schema`
59
and `pg_catalog` rather than from a list maintained beside it, so a migration
60
that adds a `machine`-named durable surface fails until someone records it and
61
says why, and a ledger entry the database does not have fails as well. The wire
62
half has no query that enumerates it and is listed in `docs/taxonomy.md`
63
instead, held by the contracts that own each surface.
64
65
A retained name owes a population, and the population must be established from
66
something that cannot lie rather than from a reading of the call sites. The
67
retired AAD `sarah.machine_token.v1` had none — nothing seals a version-1 blob
68
and a sealed pairing token cannot outlive its bounded window — so the token
69
vault carries one version and one AAD. The audit actor kind `machine` looked
70
the same and is not: a paired computer authenticating to the Git plane pushes
71
under `{:machine, id}`, from a variable that no source scan for a literal
72
finds. It stays, and the proof asserts that every principal kind
73
`OpenAgents.Forge.GitHTTP.audit_actor_kinds/0` admits is an actor kind
74
`OpenAgents.Audit` accepts, so narrowing one without the other fails here
75
instead of raising after a pack is already written.
76
77
Evidence: `OpenAgents.Vocabulary`, `OpenAgents.Audit`,
78
`OpenAgents.Forge.GitHTTP`, `OpenAgents.Machines.TokenVault`,
79
`test/openagents/vocabulary_test.exs`, `test/openagents/audit_test.exs`,
80
`test/openagents/machines/token_vault_test.exs`, and
81
`test/openagents/machines_test.exs`.
82
47 83
### PERSONA-001 — Each inference uses one immutable persona artifact
48 84
49 85
Status: Current

@@ -4114,6 +4150,7 @@ contract; the invariant prose above defines the assertion, not the filename.

4114 4150
| Invariant | Executable proof |
4115 4151
| --- | --- |
4116 4152
| CANON-001 | `test/openagents/persona/source_manifest_test.exs` |
4153
| CANON-002 | `test/openagents/vocabulary_test.exs`, `test/openagents/audit_test.exs`, `test/openagents/machines/token_vault_test.exs` |
4117 4154
| PERSONA-001 | `test/openagents/persona_test.exs`, `test/openagents/providers/persona_boundary_test.exs` |
4118 4155
| PERSONA-002 | `test/openagents/context/composer_test.exs`, `test/openagents/roles_test.exs` |
4119 4156
| PERSONA-003 | `test/openagents/persona/evaluation_test.exs` |
docs/taxonomy.md modified +15 -9

@@ -528,20 +528,26 @@ its own contracts:

528 528
- The stored `staging_disposable_resources.kind` value `machine`, and the
529 529
  audit action `repository.machine_grant.updated` with subject
530 530
  `machine_grant`.
531
- The module residency `operator_machine`, the consent `machine_pairing`, and
532
  the routing-policy id `sarah.routing.policy.paired-machine.v1`. All three sit
533
  inside the digest that `module_route_receipts` stores, so renaming one makes
534
  historical receipts fail to reproduce.
531
- The `audit_events.actor_type` value `machine`. A paired computer that
532
  authenticates to the Git plane with its `smct_` token pushes and fetches
533
  under `{:machine, id}`, so the value is written, not vestigial.
534
  `OpenAgents.Forge.GitHTTP.audit_actor_kinds/0` names the principal kinds that
535
  can reach it, and `OpenAgents.AuditTest` asserts every one of them is an
536
  actor kind `OpenAgents.Audit` accepts — a containment a reading of the call
537
  sites cannot establish, because the actor there is a variable.
538
- The module residency `operator_machine` and the routing-policy id
539
  `sarah.routing.policy.paired-machine.v1`. The residency reaches
540
  `artifact.facets` and so the `artifact_digest` and `registry_digest` a route
541
  receipt stores; the policy id and the `allowed_residencies` that admit it are
542
  both inside `policy_digest`. Renaming either makes a stored
543
  `module_route_receipts` row fail to reproduce. The neighbouring
544
  `policy_facets` consent, by contrast, is shape-checked and never digested,
545
  which is why it is now `computer_pairing`.
535 546
- The `Machines.TokenVault` AAD `openagents.machine_token.v2`, bound into
536 547
  ciphertext at rest.
537 548
- `OPENAGENTS_MACHINE_TOKEN_TTL_SECONDS` and the setting key
538 549
  `:machine_token_ttl_seconds`, which live in staging and production secrets.
539 550
540
`audit_events.actor_type` used to permit `machine` and nothing ever wrote one.
541
The application no longer accepts it; the database constraint still does,
542
because narrowing a constraint over rows this release did not write is a
543
migration, not an edit.
544
545 551
**`machine` also means machine-readable** — and that sense is correct, not
546 552
residue. `GET /api/v3` publishes `contribution.machine`, and `/agents.json`
547 553
publishes `representations.machine`, each paired with `human` and each naming a
lib/openagents/vocabulary.ex added +88

@@ -0,0 +1,88 @@

1
defmodule OpenAgents.Vocabulary do
2
  @moduledoc """
3
  The ledger of durable names that keep the word `machine`.
4
5
  The product says computer. `docs/taxonomy.md` settled that, and issue #134
6
  settled what happens to the names underneath it: a `machine` name stays where
7
  something outside this release can observe or replay it, and moves to
8
  `computer` where it cannot. PostgreSQL is the clearest case of the first —
9
  a table, column, constraint, or index name is read by rows an earlier release
10
  wrote — so this module enumerates every durable name that is exempt, and
11
  `OpenAgents.VocabularyTest` derives the live population from
12
  `information_schema` and `pg_catalog` and fails when the two disagree.
13
14
  The point is not the list. The point is that a new `machine`-named durable
15
  surface cannot appear without someone adding it here and saying why, and a
16
  removed one cannot linger here pretending to exist. See `INVARIANTS.md`,
17
  CANON-002.
18
19
  This ledger covers PostgreSQL only. The wire half of the same decision — the
20
  controller protocol `openagents.computer.v1`, the `.v1` tool schemas, the
21
  published response keys, the PubSub topics and Horde registry keys — is held
22
  by its own contracts, because no query enumerates it.
23
  """
24
25
  @tables ~w(machine_pairings machines repository_machine_grants)
26
27
  @columns [
28
    {"forge_assignments", "machine_id"},
29
    {"inference_grants", "machine_id"},
30
    {"machine_pairings", "machine_id"},
31
    {"repository_machine_grants", "machine_id"},
32
    {"work_jobs", "machine_id"}
33
  ]
34
35
  @constraints [
36
    {"forge_assignments", "forge_assignments_machine_id_fkey"},
37
    {"inference_grants", "inference_grants_machine_id_fkey"},
38
    {"machine_pairings", "machine_pairings_machine_id_fkey"},
39
    {"machine_pairings", "machine_pairings_pkey"},
40
    {"machine_pairings", "machine_pairings_status_check"},
41
    {"machine_pairings", "machine_pairings_tier_check"},
42
    {"machine_pairings", "machine_pairings_user_id_fkey"},
43
    {"machines", "machines_pkey"},
44
    {"machines", "machines_status_check"},
45
    {"machines", "machines_tier_check"},
46
    {"machines", "machines_token_expiry_after_creation"},
47
    {"machines", "machines_user_id_fkey"},
48
    {"repository_machine_grants", "repository_machine_grants_created_by_user_id_fkey"},
49
    {"repository_machine_grants", "repository_machine_grants_machine_id_fkey"},
50
    {"repository_machine_grants", "repository_machine_grants_operations_allowed"},
51
    {"repository_machine_grants", "repository_machine_grants_operations_present"},
52
    {"repository_machine_grants", "repository_machine_grants_pkey"},
53
    {"repository_machine_grants", "repository_machine_grants_repository_id_fkey"},
54
    {"work_jobs", "work_jobs_machine_id_fkey"}
55
  ]
56
57
  @indexes [
58
    {"forge_assignments", "forge_assignments_machine_id_index"},
59
    {"forge_assignments", "forge_assignments_one_active_machine_index"},
60
    {"inference_grants", "inference_grants_machine_id_index"},
61
    {"machine_pairings", "machine_pairings_code_digest_index"},
62
    {"machine_pairings", "machine_pairings_expires_at_index"},
63
    {"machine_pairings", "machine_pairings_pkey"},
64
    {"machines", "machines_pkey"},
65
    {"machines", "machines_token_digest_index"},
66
    {"machines", "machines_user_id_index"},
67
    {"repository_machine_grants", "repository_machine_grants_machine_id_index"},
68
    {"repository_machine_grants", "repository_machine_grants_pkey"},
69
    {"repository_machine_grants", "repository_machine_grants_repository_id_machine_id_index"},
70
    {"work_jobs", "work_jobs_machine_id_inserted_at_index"}
71
  ]
72
73
  @doc "Tables whose name keeps `machine`."
74
  @spec tables() :: [String.t()]
75
  def tables, do: @tables
76
77
  @doc "`{table, column}` pairs whose column name keeps `machine`."
78
  @spec columns() :: [{String.t(), String.t()}]
79
  def columns, do: @columns
80
81
  @doc "`{table, constraint}` pairs whose constraint name keeps `machine`."
82
  @spec constraints() :: [{String.t(), String.t()}]
83
  def constraints, do: @constraints
84
85
  @doc "`{table, index}` pairs whose index name keeps `machine`."
86
  @spec indexes() :: [{String.t(), String.t()}]
87
  def indexes, do: @indexes
88
end
test/openagents/vocabulary_test.exs added +100

@@ -0,0 +1,100 @@

1
defmodule OpenAgents.VocabularyTest do
2
  @moduledoc """
3
  CANON-002. Proves the `machine` exemption ledger against the live database.
4
5
  The population comes from `information_schema` and `pg_catalog`, not from a
6
  list someone maintained beside the ledger, so a `machine`-named table,
7
  column, constraint, or index added by any migration fails here until it is
8
  recorded in `OpenAgents.Vocabulary` with a reason.
9
  """
10
  use OpenAgents.DataCase, async: true
11
12
  alias OpenAgents.Vocabulary
13
14
  @pattern "%machine%"
15
16
  test "every machine-named table in the database is on the ledger" do
17
    assert live_tables() == Enum.sort(Vocabulary.tables())
18
  end
19
20
  test "every machine-named column in the database is on the ledger" do
21
    assert live_columns() == Enum.sort(Vocabulary.columns())
22
  end
23
24
  test "every machine-named constraint in the database is on the ledger" do
25
    assert live_constraints() == Enum.sort(Vocabulary.constraints())
26
  end
27
28
  test "every machine-named index in the database is on the ledger" do
29
    assert live_indexes() == Enum.sort(Vocabulary.indexes())
30
  end
31
32
  test "the ledger claims nothing the database does not have" do
33
    live = MapSet.new(live_tables())
34
35
    for table <- Vocabulary.tables() do
36
      assert MapSet.member?(live, table), "ledger names a table that does not exist: #{table}"
37
    end
38
39
    tables = MapSet.new(live_tables() ++ table_names())
40
41
    for {table, name} <- Vocabulary.columns() ++ Vocabulary.constraints() ++ Vocabulary.indexes() do
42
      assert MapSet.member?(tables, table),
43
             "ledger entry #{name} names a table that does not exist: #{table}"
44
    end
45
  end
46
47
  defp live_tables do
48
    """
49
    SELECT table_name FROM information_schema.tables
50
    WHERE table_schema = 'public' AND table_name LIKE $1
51
    """
52
    |> rows([@pattern])
53
    |> Enum.map(&hd/1)
54
    |> Enum.sort()
55
  end
56
57
  defp live_columns do
58
    """
59
    SELECT table_name, column_name FROM information_schema.columns
60
    WHERE table_schema = 'public' AND column_name LIKE $1
61
    """
62
    |> rows([@pattern])
63
    |> pairs()
64
  end
65
66
  defp live_constraints do
67
    """
68
    SELECT c.conrelid::regclass::text, c.conname
69
    FROM pg_constraint c
70
    JOIN pg_namespace n ON n.oid = c.connamespace
71
    WHERE n.nspname = 'public' AND c.conname LIKE $1
72
    """
73
    |> rows([@pattern])
74
    |> pairs()
75
  end
76
77
  defp live_indexes do
78
    """
79
    SELECT tablename, indexname FROM pg_indexes
80
    WHERE schemaname = 'public' AND indexname LIKE $1
81
    """
82
    |> rows([@pattern])
83
    |> pairs()
84
  end
85
86
  defp table_names do
87
    """
88
    SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'
89
    """
90
    |> rows([])
91
    |> Enum.map(&hd/1)
92
  end
93
94
  defp rows(sql, params) do
95
    %Postgrex.Result{rows: rows} = Ecto.Adapters.SQL.query!(OpenAgents.Repo, sql, params)
96
    rows
97
  end
98
99
  defp pairs(rows), do: rows |> Enum.map(fn [a, b] -> {a, b} end) |> Enum.sort()
100
end

This page updates live while a promote is in flight · changelog