Skip to content

Enforce T3 sign-off records for --allow-t3 (default on, private record path, no opt-out) - #123

Open
CompleteDotTech wants to merge 124 commits into
mainfrom
issue/23
Open

CompleteDotTech wants to merge 124 commits into
mainfrom
issue/23

Conversation

@CompleteDotTech

@CompleteDotTech CompleteDotTech commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Resolves #23
Resolves #24
Resolves #25
Resolves #26
Resolves #27

Change and affected skills

--allow-t3 is no longer a trust-only claim. For T3 actions the skill reads a sign-off record and enforces it by default, with no opt-out flag or environment variable (passing --require-t3-signoff is an argparse error, as on main).

  • Record and check: a documented record format and private location (inside the estate resolver root, with an explicit file override); check_owner reads and verifies a valid, unexpired, scope-matching record, rejects future and inverted dates and reports malformed covering records as invalid; approved values that are not booleans next to a valid grant fail closed as t3-signoff:invalid; a later literal approved: false withdraws a grant (t3-signoff:revoked); owner and repository spellings are canonicalized (case, whitespace, trailing slash, .git, former logins), and an invalid remaining name fails closed.
  • Sign-off ids: jobs and change records carry signoff_id (signoff_ids when several); approval_id is never set from a sign-off. Both source and destination grants are kept for star-List items and transfers, and the board-owner and repository-owner ids both appear on README jobs.
  • Per-repository authorization: every repository referenced by a copy-plan, whether explicit or captured, is authorized once on its own resolved owner before any README, workflow Auto-add, settings or view-filter job is built; withdrawn or missing repositories are omitted or held (including with no --allow-t3 flag), granted and regranted ones carry their grant ids, and standalone read-only project readme is unchanged. A populated capture with denied entries never falls back to a generic main.
  • Latest fixes in this head: a repository qualifier inside GROUPING parentheses of a copied view filter (for example (repo:Pick-Your-AI/withdrawn-x) workstream:Research) is now authorized like a bare qualifier: grouping punctuation is parsed outside quoted values (including nested groups), missing or withdrawn grants hold the view mutation, granted and regranted ones carry their sign-off ids, quoted literals and negative qualifiers are unaffected and the T1-only case is unchanged. Earlier in this PR: the .github profile destination is authorized before the profile README and data.json are written; granted former-label (renamed) Org options keep their sign-off id; a captured Org option whose name resolves to a known T3 owner is recognized independently of its description; stored withdrawal scopes in non-canonical spellings are enforced; held pin order is suppressed while authorized pins are kept; content-action proposals carry their sign-off id without being promoted to queued jobs.
  • Field-name note for E1.1.3 Carry the sign-off id into writer jobs and change records #26: the task says "add approval_id"; jobs carry signoff_id instead, and a clarification comment was posted on E1.1.3 Carry the sign-off id into writer jobs and change records #26. The acceptance criteria are unchanged.
  • Tests use synthetic records only; no real sign-off file is created and no consent is inferred. T3 approval is process-wide state (not safe for concurrent or embedded use) #28 and Land PR #20 and document the global --allow-t3 #31 are related but not closed by this PR.

Prior work

Searched open and closed PRs and issues again at this head (t3 signoff, sign-off record, allow-t3, view filter consent): this builds on merged #20 under epic #22; #124, #126 to #129, #131 and #133 are merged and unrelated; no other open PR covers the record enforcement (open #13 and #133 are unrelated).

Evidence (head 1d5f931c6e4d30b9b4cb624edef55e339c9e79e8, base main 2243010d67725c17a163223d84f91a119f7262dd)

  • Exact-head VM (designated VM, run by the outer coordinator): package tests, package validator, full pytest and quality validator all passed: 1747 passed, 11 skipped, 37 subtests (vm-1d5f931c...-result.json).
  • Consent acceptance (consent-complete118-1d5f931c...json): all 118 assertions pass (the earlier 108 plus 10 grouped-filter checks), using actual project copy-plan and project estate-plan CLI invocations (stdout and --jobs), the estate wrapper and direct-library cases with synthetic data and no GitHub writes. Earlier heads each failed a subset (for example 331b517 fails 8 grouped-filter checks); those receipts and reviews are history.
  • Native Windows host run (supplemental, owner log on this head): 1740 passed, 18 skipped, 37 subtests; package unittest OK and the package validator passing.
  • Skips: the 11 VM skips are the pre-existing absent external or private fixtures.
  • Hosted CI: read back from the checks tab before merge (not asserted here).
  • Not run: mmdc/Mermaid rendering (tool unavailable); private deny-list check (no private fixtures); pyright (no config).

Status: published for review. Independent reviews of this exact head (two Opus and Sol), hosted CI and the merge gate are outstanding; do not merge on the strength of this description. Shared validators (scripts/validate_package.py, tests/test_package_contract.py) are byte-identical to main.

Boundaries

Activation keys, approval gates, specialist entry points and the command contract are unchanged. Consent is stricter, not looser. No registry release, visibility change or live external action is implied.

Family consistency

Single-repository change; no sibling skill PRs are affected.

🤖 Generated with Claude Code

CompleteDotTech and others added 30 commits October 30, 2026 04:04
Document the global --allow-t3 flag (before the command) in SKILL.md
(synopsis, Command Reference, co-owner sign-off paragraph and the
refusal sentence), the Unreleased CHANGELOG, and the org and projects
references. The flag applies to every repository command, generator,
doctor, lint and the org and project groups. It lifts only the T3
refusal for one invocation; excluded and HOLD owners stay refused.

Add tests/test_allow_t3_docs.py to check the synopsis, the refusal
text, the CHANGELOG entry and the --help output. No behaviour change;
scripts, the shared package validator and its contract test are untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Document the global --allow-t3 flag (before the command) in SKILL.md:
the command synopsis, the refusal sentence, and a co-owner sign-off
paragraph. The flag applies to every repository command, generator,
doctor, lint and the org and project groups, and meta, org and project
also accept it after the command name. It lifts only the T3 refusal for
one invocation; excluded and HOLD owners stay refused, and it is never
read from a config file or environment.

Add the flag to the Unreleased Added list in CHANGELOG.md and note that
the global flag is equivalent in references/org.md and
references/projects.md.

Add tests/test_allow_t3_docs.py to check the synopsis, the refusal text,
the CHANGELOG entry and the --help output. No behaviour change; scripts,
the shared package validator and its contract test are untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Move the Unreleased changelog entry from Fixed into the Added list.

Note that HOLD is refused only for repository commands and generators.
The org about, pins, health and doctor actions pass allow_hold, so they
admit a HOLD org with or without the flag.

Make test_help_text_matches_docs cross-check the --help usage line
against the SKILL.md synopsis, so it fails on a base without the doc
change. Also check the changelog entry sits in the Unreleased Added list.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

# Conflicts:
#	SKILL.md
#	tests/test_allow_t3_docs.py
…ses T3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…exceptions

doctor forces R7=hold only without the flag; local meta and fingerprint
do not check owner eligibility. Pinned by a docs test. No behaviour change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the process-wide _T3_SIGNED_OFF module global with a ContextVar
so --allow-t3 sign-off cannot leak between concurrent or embedded calls.
set_t3_signed_off now returns a token, reset_t3_signed_off restores the
prior value, and showcase.main() wraps each invocation in that token.
Default stays unsigned; CLI behaviour is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the flag (or SHOWCASE_REQUIRE_T3_SIGNOFF=1), --allow-t3 is honoured only when decisions/t3-signoffs.yaml in the estate bundle holds an approved, unexpired record for the owner (and repository when scoped); otherwise exit 2 with t3-signoff:missing|expired|scope. Writer jobs carry signoff_id/approval_id and doctor shows signoff_id. Default behaviour unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d ids, honour aliases, show sign-off in doctor (#23)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ied scopes, strict expiry, malformed scopes, repo threading, report stamping (#23)

Fresh VM verification: not run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… malformed sign-off files, test HOLD owners (#23)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests/test_allow_t3_global.py moves from CRLF (the only CRLF Python file
at base) to the repo's LF convention; review with --ignore-cr-at-eol.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Integrate the T3 sign-off record check with #28 and #31:
- signoff.py state (required flag, grants, aliases) is context-local
  (ContextVar), matching #28's --allow-t3, so one thread's opt-in or
  grants cannot leak into another's owner gate. showcase.main() opens a
  fresh run state and restores the enclosing one on exit. Tests:
  test_signoff_state_is_context_local (fails with a module-global
  required flag) and test_main_restores_an_enclosing_runs_signoff_state.
- config.t3_signed_off(owner, repo) reads the ContextVar and defers to
  signoff.granted under the opt-in flag.
- SKILL.md synopsis lists [--require-t3-signoff] and the #31 sign-off
  paragraph names its refusal codes; test_allow_t3_docs follows --help.
- content.py and the new test file keep LF endings (issue/23 had
  converted content.py to CRLF, a whole-file diff on a shared file).
- The default record path decisions/t3-signoffs.yaml is marked in
  signoff.py as the working default pending the maintainer's choice.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-write grants, stamped change records (#23, #24, #26)

- --t3-signoff-file PATH / SHOWCASE_T3_SIGNOFF_FILE: no default location (the canonical
  bundle path is the maintainer's choice); strict mode without a file refuses t3-signoff:no-file.
- references/t3-signoff.schema.json + references/t3-signoff.md: id, owner, repos, approved,
  approver, approved_on, expires, note. Records breaking it refuse t3-signoff:invalid with the
  first problem instead of a silent t3-signoff:missing. Synthetic fixtures under tests/fixtures.
- Timestamp expiries compare as aware instants; naive timestamps are invalid; date expiries stay
  inclusive through that day.
- Grants and aliases are copy-on-write ContextVar values, so inherited asyncio contexts no longer
  leak a child's grant into the parent.
- Change records get signoff_id (no approval_id), in --records files and JSON output; star-List
  jobs that org pins plans on the personal account carry the T3 org's sign-off explicitly.
- Docs: SKILL.md, CHANGELOG, constraints.md, ONBOARDING.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…m jobs, refuse mixed-type keys (#23, #26)

- signoff._parse_when: a string must match the schema's date or awareTimestamp
  pattern before parsing, so compact (20991231), week (2099-W52-1) and +00
  offset forms are invalid at runtime as they are in the schema. A test applies
  the schema's patterns with re, so it runs without jsonschema.
- signoff.problems: sort unknown keys with a type-aware key, so a record with
  `unexpected: true` and `7: unexpected` refuses t3-signoff:invalid instead of
  raising TypeError.
- org star-List item jobs that add another owner's T3 repository now carry
  that repository's record as signoff_id/approval_id (signoff.stamp_source),
  with a T1-list/T3-favorite regression test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…offs, enforce the record-file envelope, restore LF (#23, #26)

- org labels: a member whose approved pending transfer resolves to a T3 org passes
  check_owner for that org (held otherwise); its label jobs carry the record.
- _t3_held checks the favorite's current slug as well as the named one.
- topics --also-in: the record that admitted a T3 membership topic is carried
  onto the change record (signoff_id; signoff_ids when several apply).
- signoff.load_records rejects files whose envelope breaks the schema
  (top-level list, extra top-level key, non-object entry): t3-signoff:missing.
- CHANGELOG.md and references/t3-signoff.md back to LF; .gitattributes pins them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…dopt and estate-plan associations, restore CRLF (#23, #26)

- org: the transfer-target T3 checks (labels_plan gate, _t3_held current slug)
  now run only under --require-t3-signoff; the flag-off default matches base
  2be04ac (new flag-off test). The CHANGELOG "Fixed" entry moves into the
  opt-in flag's entry.
- org.public_slice: under the flag, a home repository a T3 org holds today
  (approved pending transfer) is omitted until that org's record covers it.
- signoff.stamp_associations: shared association stamping (cmd_topics,
  intake.adopt, project.estate_plan); keeps an existing approval_id.
- adopt: topics job and change record carry topics.merge signoff_ids.
- project.estate_plan: T3 orgs keep their record id; jobs naming them carry it.
- tests/test_allow_t3_global.py: base CRLF restored, diff vs base is +108/-0.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ists and aliases, match estate orgs by whole name, log ignored record file (#23, #25, #26)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…path

Rewrite references/t3-signoff.md to the agreed interface: --require-t3-signoff
is on by default, the canonical record file is <estate_root>/decisions/
t3-signoffs.yaml (resolver.estate_root(); SHOWCASE_ESTATE_ROOT or the skill's
parent bundle), and --t3-signoff-file / SHOWCASE_T3_SIGNOFF_FILE override it.
Removes the resolved open-decision section. Format, YAML example and
synthetic-fixture note are unchanged.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rd path (#23, #24, #25, #26, #27)

--allow-t3 now needs a valid, unexpired record by default. The record file resolves
as --t3-signoff-file, then SHOWCASE_T3_SIGNOFF_FILE, then
<estate root>/decisions/t3-signoffs.yaml; a missing file or unavailable estate root
refuses t3-signoff:no-file naming the path and the tool never creates the file.
--no-require-t3-signoff (or SHOWCASE_REQUIRE_T3_SIGNOFF=0) is the explicit, logged
opt-out. OWNER/NAME repositories are covered only by the record's own org, and
estate-plan stamping ignores display names shared with another org. Existing
trust-only tests use a synthetic record fixture; docs and CHANGELOG updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CompleteDotTech and others added 13 commits November 1, 2026 02:39
…repo-canonicalization-50f) regressions and docs

Owner integration of Haiku results ef2d8ac (signoff.canonical_repo, evaluate/_covers canonicalization) and 796bd75
(project copy-plan --repos validation, README entry normalisation, no-flag T3 README omission).
- canonical_repo rejects `.`/`..` owners (`../x` fails closed).
- Tests: evaluate spellings (x/, ' x', 'x ', x.git, x/.git) beside a `*` grant plus withdrawal; invalid names fail
  closed; CLI spellings (slash, .git, comma-space, bare, padded owner) omit withdrawn-x from stdout and --jobs; malformed
  --repos usage refusal; no-flag cross-owner omission (test_control_without_allow_t3_path_unchanged inverted).
- Docs: references/t3-signoff.md spelling rules and issue #24 field mapping; CHANGELOG entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The pins order job tells the writer to make the ticked set exactly the
kept list and untick every other repository. That would also withdraw a
pin held for co-owner sign-off. Track targets with a T3 sign-off hold and
skip the exact-set order job for them, logging pins-order-held:<target>:t3-signoff.
The individual authorized pin jobs are unchanged.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…bad entries

Stored grant and withdrawal scopes are now canonicalized the same way as the
requested repository (_canon_entries, `*` preserved), in _covers, _scope_key
and _withdrawn. `site/`, ` site` and `site.git` name `site`. An entry that
cannot be canonicalized makes the record malformed (problems() reports
`repos:`), so a withdrawal with such a scope is never silently ignored.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…es and held pin order

Owner integration of round 13: _finish and showcase main stamp writer-shaped
proposals (kept out of the writer queue); tests for canonicalized stored
scopes, invalid booleans, held exact-set pin order and proposal stamping;
docs and CHANGELOG.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Remove the f prefix from a placeholder-free string literal on line 1314
(ruff F541). No behaviour change.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…them

Holds a view-filter mutation that names a repository without a valid T3 sign-off
and attaches the covering sign-off ids to permitted filter jobs.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rs; tests and docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CompleteDotTech and others added 15 commits November 1, 2026 04:20
copy_plan rediscovered captured Org-option provenance from the rendered
README text, which loses the grant when a former login has a different
captured label. The README job now receives the signoff_id of every
retained_t3 entry through signoff.stamp_associations, merged with ids
already on the job.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
public_slice excludes .github, so its repository check never saw the
profile's own destination. With --allow-t3 on a T3 owner, a refused
sign-off for <owner>/.github now raises Refused (log
t3-signoff:<code>:<login>/.github) before the README, data.json or
assets are built or written.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ance regressions

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ptured Org options

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ly; carry labels to Org-option stamping

captured_t3 tries the description remainder (T3 prefix only) and then the option
name, so an unresolved description no longer hides a recognized T3 name. Retained
orgs carry their captured labels; an org retained from two options merges them.
_stamp_estate_orgs credits a job that quotes a label as a whole token, so the
Org-options mutation carries the retained org's signoff_id.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ame independently)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…esolution and Org-options stamping; docs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
… view filters

_filter_repo_refs tokenized the raw filter, so a positive repo: qualifier
wrapped in parentheses (e.g. '(repo:A/x) workstream:Research') was not
seen and its repository skipped sign-off enforcement and provenance
stamping. Grouping parentheses outside quoted values are now replaced
with spaces before tokenizing; quoted values are left unchanged.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
… permitted records

Adds regressions for repo qualifiers wrapped in grouping parentheses:
parser cases for _filter_repo_refs, withdrawn holds with parity against
the ungrouped filter, missing-record holds, a permitted repo carrying
the sign-off id, and a T1-only control. The new grouping cases fail
against the current parser (the P1 bypass) until the production fix lands.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
… parentheses

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant