Repository navigation
Publisher trust and the name-lookalike check for customer MCP (#357, part b) - #361
Conversation
State the connection grant record that the custody keeps (spec RFC 0030 §5, a Draft): its members, its least-privilege, owner, idle and revocation rules, the operator's commands, and the departures from the RFC. The RFC led. This section and the design state what the implementation in the next commit keeps. It is not a receipt and changes no signed record. Refs #357 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
…dle lifecycle (#357) Each Drive, Gmail, Notion, Databricks and Azure DevOps connection has a grant record in its custody (connection-grants.json, kept beside the state). The record holds the owner and its issuing host, the required, granted and used scopes, the last use, an optional idle expiry, and the state: active, quarantined or revoked. It carries no credential, token, account or content. - excess-scope: a Google token stating more than the scope requested is refused by name, at consent and at renewal (drift). So is a Databricks service principal configured with a scope its operations do not declare, and a connection holding one is refused at each use. - Each dispatched acquisition or action records the declared scopes it used. The report lists the granted scopes that were not used. - Before each acquisition or action through an owned grant, an owner read goes to the issuing host. removed applies the removal and refuses (owner-removed); no answer refuses (owner-unknown). A grant with no owner record is used as before and reported as owner-missing. - An idle expiry is enforced at the next use and by a sweep; equal time counts as expired. Owner removal revokes or quarantines, as the operator chooses. A quarantined grant admits no call. - Revocation deletes the credential and calls Google's RFC 7009 endpoint. Elsewhere, or until the provider confirms, the record says provider-grant-may-persist. No pending-change journal is touched. - gateway-connections --grants OPERATION gives the operator report, set-owner, set-idle-expiry, owner-reads, remove-owner, revoke and sweep. Refs #357 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
A customer binding may declare its server's publisher: an npm package, a GitHub repository or a domain. The binding digest covers it. Its liveness is read when the server is registered (gateway connect, a --replace recapture included) and on the operator's schedule (gateway publishers --check): - registry: a deprecated latest version or a missing package is unmaintained; - repository: an archived, disabled or missing repository is unmaintained; - domain: an expired (equal time counts), pending-delete or unregistered domain is unmaintained. What was read is kept in <engine.json>.publishers.json (mode 0600), by platform and by binding. It is shown with the approval, and an unmaintained publisher is reported as publisher-unmaintained. Under the operator's refuse policy (gateway publishers --policy refuse), the registration is refused, and so is each acquisition before its source starts and each action before an executor runs. A record that cannot be read refuses. A registration whose name closely matches one of the gateway's own integrations under another publisher is flagged name-lookalike, after folding case, punctuation and look-alike characters: one edit away (two for a long name), or containing the name. RFC 0030 §5 rule 7 led; SPEC §10 rule 8, the connection-grants design and engine-config state it in this commit. Refs #357 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
…#357) Windows reports no Unix permission bits, so the 0600 assertion holds on Linux and macOS only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
|
Verdict: #360 — changes required before merge (2 HIGH: H1, H2; 2 MEDIUM; 4 LOW). #361 — changes required before merge (1 HIGH: H3; 5 MEDIUM; 5 LOW). Security clean room: gateway #360 and #361 (#357, parts a and b)
Method
FindingsHIGH
MEDIUM
LOW
Established as holding
Mutants (mine, beyond the builder's 32)
Not established
|
…iew H1, H2, M1, M2, L1, L4)
The review of record found that idle expiry and the owner read gated
acquisitions only. A search, a selection's read or a connection test
reached the provider with an expired grant, or with a grant whose owner
had been removed.
- H1, H2: every read of a grant's credential now passes the full check
(grantCredential):
- in provider.access for Drive, Gmail and Notion (search, storage and
reads);
- at the Databricks token, covering reads, connection tests and MCP
tests;
- at each Azure DevOps read and test.
The full check covers state, idle expiry (which revokes), excess and
the owner read. An acquisition's check marks its call, so its
credential read re-reads the state without a second owner read. An
owner's grants in other providers' custody are refused at their own
next use of any kind.
- M1: Databricks declares the narrowest scopes: sql for a statement,
unity-catalog for catalogs, and each managed MCP server's page scope.
all-apis is admitted but recorded in wider and reported as
wider-than-needed. It is never taken as used by an MCP call.
- M2: the production owner-read client is pinned (no redirect, 200
only), and so is an unreadable grant time counting as expired.
- L4: the Databricks grant state is re-read once a token is held,
immediately before dispatch.
- L1: the docs now say that Notion is held to excess-scope at connect,
and that its renewal refuses as provider-unavailable.
SPEC §10 and the design note change with the code. The review led.
Refs #357
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
…iew H3, M3-M7, L9) - H3: every read-modify-write of the publisher record is made under the configuration's lock, the one connect holds. connect writes the record under its lock once the configuration is in place (afterWrite). gateway publishers --check makes its liveness reads without the lock, then merges what it read into the record as it stands under the lock. A policy set during a re-check is no longer lost. - M3: a 404 is no longer taken as a lapse. A package or repository that is not public, or a domain no RDAP service answers for, is unverifiable, reported as publisher-unverifiable and never refused. A host name is read at the first registration above it, so the design note's own example (tickets.acme.example) reads as maintained. - M4: the unreadable-record refusal answered to /acquire and /act names no path. - M5: the record is given the configuration's mode and owner through the descriptor, as a replaced configuration is. A root connect over a configuration the signer owns leaves a record the signer can read. - M6: fullwidth letters, and Cyrillic and Greek letters (capitals and small letters) drawn like a Latin letter, are folded before the lookalike comparison. - M7: npm's deprecated: true and RDAP's redemption period are pinned by tests. - L9: the parse error now reads "publisher is declared only on a customer binding". The departure list says that no policy refuses a lookalike. The review led. SPEC §10 rule 8, the design note and engine-config change with the code. Refs #357 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Re-read after the fix roundFinal verdict:
The re-read
The first review's findings
New findingsHIGH
MEDIUM
LOW
Not established in the re-read
|
…l (re-read N7, N6, N5) - N7: the credential gate first holds the ledger to custody, and saves it where reconciling changes it. A Databricks or Azure DevOps connection made again under its id, after its grant was revoked, is then a new grant at its gate, as it already is in the report. Before this, it stayed grant-revoked until a sweep. - N6: a Google renewal now re-reads the grant's state once the token is renewed, immediately before the token is used, as Databricks does after its mint. A quarantine made during the renewal refuses that call. - N5: SPEC §10 rule 2 now says that a scope admitted as wider than needed is recorded as used for an operation it covers when none of that operation's declared scopes is granted. That is Databricks' all-apis, for a statement or a catalog listing, and never for an MCP call. Refs #357 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
…stry (re-read N1-N4)
- N1, N2: the walk up from a host is gone. A domain publisher is now the
registrable domain, refused at parse otherwise.
- The name has exactly two labels, or three under a two-label public
suffix of a fixed, partial list.
- A host under a registrable domain is refused, and the message names
the domain to declare. A public suffix itself is refused too.
- A shared platform's domain (vercel.app and others), and any host on
one, is refused; such a server declares its repository or package.
- Liveness is read from the registry's own RDAP service, as the IANA
bootstrap names it, not through a redirector.
- The registry's 404 for the declared domain is unmaintained ("not
registered"): the name is free for another to register. Only a
top-level domain with no RDAP service is unverifiable. A public
suffix's own object is never read for a registration under it.
- N3: under the lock, gateway publishers --check keeps a platform's
record written since the re-check read it, whether by a connect or a
recapture, of another binding or at another time. Its stale reading no
longer replaces that record.
- N4: Latin letters with a diacritic (Latin-1 Supplement and Latin
Extended-A), mathematical alphanumerics, small capitals and a few
Armenian letters are now folded before the lookalike comparison. A
decomposed spelling's combining mark was already dropped by the final
letter filter. Other letters are listed as known misses.
The re-read led. SPEC §10 rule 8 and the design note change with the
code.
Refs #357
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Confirmation of the second fix roundFinal verdict:
The confirmation
Items
New findingsHIGH
MEDIUM
LOW
Not established in the confirmation
Disposition#360 merges at #361 is held for N9 (HIGH). N8 (MEDIUM) and N10 (LOW) are fixed in the same round, and the same reviewer confirms that round before merge. This is a Claude Opus review of a Claude Opus build, not a cross-vendor claim. |
|
Carry needed from base commit f37fe41. |
Bring #360's squash (f37fe41) into #361. Its content equals the grant-hygiene-357 head (ac9bead) already merged here, so the conflicts on SPEC.md and docs/design/connection-grants.md resolve to this branch's versions, which hold that content and #361's own lines. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…newal re-reads (confirmation N9, N8, N10, N6a) - N9: an inconclusive reading (a failed, timed-out, refused or rate-limited read, an answer that cannot be read, or unverifiable) never lifts a conclusive one of the same declared publisher. The record keeps the last conclusive state and its time, and records the failed read beside it (lastAttempt, attemptState, attemptReason), reported as publisher-read-inconclusive. gateway publishers --check warns on stderr and exits 3. Under refuse, an unmaintained publisher stays refused, a recapture included, until a conclusive maintained reading. A platform with no conclusive reading yet takes the inconclusive state, and is not refused. - N8: a name of three labels or more under a suffix the gateway does not list is refused without naming a domain to declare; the message says to declare the repository or package, or none. The suffix list adds nhs.uk, police.uk, sch.uk, mod.uk, the Spanish com.es-style suffixes and the Canadian provinces (leedsth.nhs.uk is accepted, nhs.uk refused). The docs say the list is partial, and that declaring an unlisted suffix is meaningless. - N10: an empty bootstrap, or one that lists the top-level domain with no HTTPS service, is unknown (inconclusive), not unverifiable. - N6a: the grant's state is re-read once a token is held after the remaining renewal paths: another process's renewal taken in place of a failed one, and Notion's renewal. The confirmation led. SPEC §10 rule 8 and the design note change with the code. Refs #357 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Final confirmation (#361)Final verdict:
Method
Items
New findings (LOW)
Disposition#361 merges at |
|
🎉 Thank you for your contribution to Judgment Pack! If you’d like another task, choose a path that interests you: |
Closes #357, with #360 (merged). This is part (b), milestone
v0.24.0. It covers publisher trust and the name-lookalike check for customer MCP. It also carries one #360 residual: the grant's state is now re-read after every token-renewal path.Out of scope: delegated per-run credentials (#356) and #352 M6. The receipt
delegationmember is not touched.What changed
Declaration. A customer binding (#348) may declare
"publisher": {"kind": "registry"|"repository"|"domain", "name": ...}. The accepted names arenpm:<package>,github.com/<owner>/<repository>, or a registrable domain. The member is refused on a non-customer binding, and the binding digest covers it.What a domain publisher may be. It is a registrable domain (
acme.example,acme.co.uk), refused at parse otherwise:co.uk,nhs.uk,com.es,on.ca, …), and declaring an unlisted suffix is meaningless;vercel.app,github.ioand others), or any host on one, is refused; such a server declares its repository or package.Liveness:
registry(npm): a deprecated latest version (a message ortrue) is unmaintained. A 404 isunverifiable, since the package may be private or unpublished.repository(GitHub): archived or disabled is unmaintained. A 404 isunverifiable.domain: the registry's own RDAP service is read, as the IANA bootstrap names it for the top-level domain.unverifiable. A bootstrap that cannot be read, is empty, or lists the TLD without an HTTPS service isunknown.unknown. A binding with no publisher isundeclared.Recorded, shown with the approval, and re-checked:
gateway connectreads the publisher before any adapter runs (recapture included) and shows the lines with the approval.<engine.json>.publishers.json, with the configuration's own mode and owner, so a signer that can read the configuration can read the record.gateway publishers --checkreads every customer platform again, on the operator's schedule.<engine.json>.lock).--checkmerges under the lock, keeping a policy set meanwhile, and a platform's record written meanwhile by aconnector recapture.An inconclusive reading never lifts a conclusive one.
unknownorunverifiablereading of the same declared publisher keeps the lastmaintainedorunmaintainedstate and its time.lastAttempt,attemptState,attemptReason) and reported aspublisher-read-inconclusive.--checkwarns and exits 3.refuse, an unmaintained publisher stays refused, a recapture included, until a conclusivemaintainedreading.Policy.
publisher-unmaintainedandpublisher-unverifiableare always reported. Undergateway publishers --policy refuse, a record of the platform's own binding that saysunmaintainedrefuses:/acquire, before the source starts;/act, before an executor runs.unverifiable,unknownandundeclaredare never refused. An unreadable record refuses aspublisher-record-unreadable, with no path in the reason.reportis the default (open question 15).name-lookalike. A registration is flagged when its platform name, or its binding's platform name, closely matches a shipped integration under another publisher. Names are compared after folding:0→o,1→l,3→e,5→s,rn→m,vv→w.A close match is equal to the integration's name, one edit away (two for a name of ten characters or more), or containing it. It is never a refusal. Letters outside the fold tables are documented as known misses.
Grant renewals (from #360's review). The grant's state is re-read once a token is held after another process's renewal, or after Notion's renewal, as it already was after a Google renewal and a Databricks mint (
adapters/connections/provider.go).Public surfaces changed:
publisher;gateway publishers;<engine.json>.publishers.json;publisher-unmaintainedandpublisher-record-unreadable;publisher-unverifiable,publisher-read-inconclusiveandname-lookalike;gateway publishers --checkexit status 3 on an inconclusive reading;connectlines.These are documented in SPEC.md §10 rule 8,
docs/design/connection-grants.mdanddocs/design/engine-config.md. No receipt, seal, registry line, statement, corpus vector or catalog binding changes, and no boundary path is touched.Departures from RFC 0030 §5 rule 7
undeclared; the RFC hasnull.Review
The security clean room and its re-read are posted on this pull request. All findings the build took on are fixed, with mutation-checked tests:
trueand redemption are pinned;unknown;Mutants: 15 in the build, 11 in the first fix round, 8 in the second, and 9 in the confirmation round (7 core, 2 adapters). All were caught. One further second-round mutant, combining marks kept, showed that code redundant, and the code was removed.
Material-decision impact: public-surface, documented-claim, security; review: same-vendor clean room and re-read (Claude Opus), not a cross-vendor record.
Checks
At the head:
go test ./...passed in full ingo/.adapters/, every package passed except the two pre-existingadapters/internal/sandboxsocket-mask tests, which fail identically onmainon the build host.-racepassed on the publisher tests.v0.24.0 release-note text (part b)
🤖 Generated with Claude Code