Contracts
acceptance.md
Contract: acceptance evidence
Every row is red-first on the merge-base through the named entry point and green at head. Blast radius per CLAUDE.md §Test policy plus tests/architectural/ in full (cross-cutting env/config change).
| ID | Scenario | Entry point | Evidence |
|---|---|---|---|
| A1 | fresh install, no config, no session, interactive status | public spec-kitty subprocess, isolated SPEC_KITTY_HOME, pty | completes; one hint on stderr; second run: no hint; JSON run: no hint |
| A2 | auth login with nothing configured | subprocess against a recording stub OAuth server | request lands on the packaged default host; target line printed with packaged default |
| A3 | target matrix (contracts/target-resolution.md) | resolve_server_target unit + auth status subprocess | every cell resolves and prints the expected source; split-brain still fails closed |
| A4 | owned checkout lane move | real repo + --owned-checkout + recording relay stub | exactly one event.publish; attrs identical to worktree case |
| A5 | unauthenticated full cycle | recording stub for SaaS + relay | 0 requests across specify → review incl. readiness |
| A6 | auth logout then lane move | same stub | 0 requests after logout; hint marker reset |
| A7 | tracker command / --from-ticket logged out | subprocess | exit non-zero with sign-in guidance; no "not enabled" text anywhere in the tree |
| A8 | retired names present in env and config | subprocess | no effect; [sync] table ignored |
| A9 | opt-outs | unit | pre-review gate skipped only by its own name; handler registration suppressed only by its own name |
| A10 | vocabulary | git grep over live src/docs/templates | zero occurrences of retired identifiers outside the occurrence-map exceptions |
| A11 | gates | tests/architectural/ full | at or below baselines; terminology guard green |
auth-visibility.md
Contract: target visibility in auth login / auth status
Both commands print, before any network activity, one line of the form:
Team Kitty target: https://team.spec-kitty.ai (packaged default)
Team Kitty target: https://spec-kitty-dev.fly.dev (config.toml [team_kitty] server_url)
Team Kitty target: https://x.example (SPEC_KITTY_SAAS_URL)
auth statusandauth whoamireuse_auth_saas_target.print_saas_target;auth logincalls the same printer.- The existing issuer-mismatch warning is unchanged.
--jsonvariants carrytarget.urlandtarget.sourcefields; no token or session value appears in any output (NFR-004).- The retired "Set SPEC_KITTY_SAAS_URL … and try again" remediation text is removed; the only remaining remediation is the split-brain message.
environment-and-config.md
Contract: environment variables and configuration after this mission
| Name | Status | Meaning |
|---|---|---|
SPEC_KITTY_SAAS_URL | kept | environment override of the hosted target |
SPEC_KITTY_SAAS_TOKEN | kept | service-token bearer (counts as authenticated) |
SPEC_KITTY_TEAM_SLUG | kept | scope selector |
SPEC_KITTY_SAAS_FANOUT_TIMEOUT | kept | fan-out bound |
SPEC_KITTY_SKIP_PRE_REVIEW_GATE | new | skip the pre-review regression gate |
SPEC_KITTY_NO_MOMENT_HANDLERS | new | register no moment handlers at import |
SPEC_KITTY_ENABLE_SAAS_SYNC | removed | unknown to the CLI; no effect, no notice |
SPEC_KITTY_SYNC_DISABLE | removed | unknown; no effect |
SPEC_KITTY_SYNC_MINIMAL_IMPORT | removed | unknown; no effect |
config.toml [team_kitty] server_url | new | configured hosted target |
config.toml [sync] server_url | removed | never read |
core/env.pyexposes two single-purpose accessors for the new names; theSYNC_DISABLE_ENV_VARStuple andfirst_set_sync_disable_envare deleted.core/secret_redaction.pyprintable allowlist andm_3_2_8_provision_kitty_env.py::GOVERNED_OPERATOR_VARSlist the new names and drop the retired ones; the migration never seeds a retired name again and never writes a value it did not observe.docs/api/environment-variables.mdis the user-facing source for this table.
moment-path.md
Contract: moment publishing under launch defaults
Unchanged wire behavior (docs/context/team-kitty.md), restated as the acceptance contract:
1. Every lane transition and lifecycle beat calls fire_saas_fanout / fire_lifecycle_saas_fanout, from lane worktrees and owned checkouts alike. OWNED_SYNC_UNSUPPORTED no longer exists. 2. With AuthenticationState = unauthenticated, credential resolution returns None before any request; a recording stub observes 0 requests across specify → plan → tasks → implement → review, including readiness. 3. With a resolvable capability, exactly one POST <relay>/managed/control (op: event.publish) per transition, 750 ms budget, no retry; failures are logged at debug and never surface to the user. 4. An owned-checkout transition produces a moment byte-identical in kind, ref, and attrs to the same transition from a lane worktree (only session_id differs). 5. SPEC_KITTY_NO_MOMENT_HANDLERS=1 registers no handlers at import; it is the only way to suppress the fan-out for an authenticated process and is documented as a test/isolation switch, not an operator feature.
saas_rollout.md
Contract: SaaS Rollout Gate
Version: 3 (#3980, Team Kitty launch defaults — the default flipped from opt-in to opt-out; version 2 relocated the canonical home from saas/rollout.py to core/saas_sync_config.py [#2252]) Module: src/specify_cli/core/saas_sync_config.py (canonical home). src/specify_cli/tracker/feature_flags.py re-exports from here (the saas/rollout.py and sync/feature_flags.py shims died with their packages, issue #5). Stability: Internal CLI surface, but public to all of src/specify_cli/. Treat as a stable internal contract.
Provenance: this is the live, self-contained version-3 contract for mission team-kitty-launch-defaults-01M1XJ4Y (#3980). The version-2 record it supersedes lives at kitty-specs/082-stealth-gated-saas-sync-hardening/contracts/saas_rollout.md and stays byte-frozen there — tests/architectural/test_archive_root_byte_identical.py (NFR-002) freezes every pre-existing file under kitty-specs/ at the port base, so a contract version bump is recorded as a NEW file in this mission's dossier, never as an edit to the archived original (same pattern as kitty-specs/dispatch-dry-run-route-only-01M1HKV2/contracts/cli-dispatch-dry-run-output.md).
Functions
is_saas_sync_enabled() -> bool
Inputs: None (reads process environment).
Returns: True unless the environment variable SPEC_KITTY_ENABLE_SAAS_SYNC explicitly opts out (launch default, #3980: on):
- Unset variable →
True - Empty / whitespace-only string →
True "1","true","yes","y","on"(case-insensitive) →True(redundant confirmation)"0","false","no","off"→False(the opt-out)- Any other non-empty value (e.g.
"banana") →False
Side effects: None. Pure function (modulo os.environ read).
Performance: O(1).
sync_active() -> bool
Returns: is_saas_sync_enabled() and not sync_kill_switch_active() — the process-wide kill switch is SPEC_KITTY_SYNC_DISABLE alone. SPEC_KITTY_SYNC_MINIMAL_IMPORT no longer disarms sync; it is a deprecated alias of the moment-handler import gate (core.env.moment_handlers_disabled_reason).
saas_sync_disabled_message() -> str
Inputs: None.
Returns: A human-readable, one-line message explaining the rollout is off and how to re-enable it. Stable wording (asserted by tests):
> Hosted SaaS sync is disabled on this machine. Unset SPEC_KITTY_ENABLE_SAAS_SYNC (or set it to 1) to re-enable it.
Related environment names (#3980 launch table)
| Name | Role |
|---|---|
SPEC_KITTY_ENABLE_SAAS_SYNC | opt-out only (=0); absent means on |
SPEC_KITTY_SAAS_URL | dev/self-host override of the packaged default (DEFAULT_HOSTED_SAAS_URL, auth/config.py) |
SPEC_KITTY_SAAS_TOKEN, SPEC_KITTY_TEAM_SLUG | service-token and scope overrides |
SPEC_KITTY_SYNC_DISABLE | the single process-wide kill switch (sync_active()), nothing else |
SPEC_KITTY_SYNC_MINIMAL_IMPORT | deprecated alias of the moment-handler gate, warns once |
SPEC_KITTY_NO_MOMENT_HANDLERS | the moment-handler import gate (status/adapters.py) |
SPEC_KITTY_SKIP_PRE_REVIEW_GATE | the pre-review regression gate's own opt-out (agent/tasks_move_task.py) |
SPEC_KITTY_SAAS_FANOUT_TIMEOUT | unchanged, documented |
Post-launch, SPEC_KITTY_SYNC_MINIMAL_IMPORT and the deprecated ENABLE spellings are removed from core/env.py, core/secret_redaction.py, and m_3_2_8_provision_kitty_env.py (#3980 post-launch section).
Backwards Compatibility Shims
The following modules continue to export is_saas_sync_enabled and saas_sync_disabled_message:
src/specify_cli/tracker/feature_flags.py
All shims re-export the same function objects (identity preserved) and add no behavior. Any future change to the env-var contract is made once in core/saas_sync_config.py.
Usage Contract for Callers
1. CLI Typer registration (src/specify_cli/cli/commands/__init__.py):
is enforced at invocation time by tracker.py's callback (tracker_callback / per-command checks), which print saas_sync_disabled_message() and exit 1 when the gate is off.
- Registration of the tracker group is unconditional; the rollout gate
2. Programmatic callers (dashboard, sync events, readiness):
- MUST call
is_saas_sync_enabled()directly OR rely onevaluate_readiness()(which checks rollout first). - MUST NOT cache the result across process boundaries — the env var is read each call by design.
3. Tests:
disabled state (deleting the variable no longer disables it — the default is on) / monkeypatch.setenv(..., "1") for the enabled state.
collection-wide authority (os.environ.setdefault("SPEC_KITTY_ENABLE_SAAS_SYNC", "1"), #3213) — the suite never depends on the packaged default or on runner ambient state, and a producer must NOT pre-set the flag to 0 in the pytest environment (that would suppress the collection-time authority and skip the import-time gated tests). The CI workflows that invoke the CLI itself outside pytest pin their own opt-out instead (e.g. .github/workflows/ci-windows.yml). The former monolithic .github/workflows/ci.yml ambient pin was retired with that workflow's modular replacement on main.
- Use
monkeypatch.setenv("SPEC_KITTY_ENABLE_SAAS_SYNC", "0")to simulate the - The autouse fixture at
tests/conftest.pysets the gate ON by default; dual-mode tests opt out explicitly. - The suite's own environment is pinned by
tests/conftest.py's
Test Requirements
NOT disarmed by SPEC_KITTY_SYNC_MINIMAL_IMPORT alone
- The rollout-gate test file MUST cover at minimum:
- Unset env var →
True - Empty string →
True "1"/"true"/"TRUE"/"yes"/"on"→True"0"/"false"/"off"→False- Garbage values (
"banana") →False sync_active()isFalsewhenSPEC_KITTY_SYNC_DISABLEis truthy, and is- The disabled message wording is byte-for-byte stable
- mypy --strict clean
target-resolution.md
Contract: hosted target resolution
Authority: specify_cli.auth.server_target.resolve_server_target (single).
env SPEC_KITTY_SAAS_URL | config.toml [team_kitty] server_url | Result | source |
|---|---|---|---|
| unset | unset | https://team.spec-kitty.ai | packaged_default |
| unset | https://spec-kitty-dev.fly.dev | dev host | configuration |
https://x | unset | https://x | environment |
https://x | https://x | https://x | environment (override mode NONE) |
https://x | https://y | fail closed: ServerTargetSplitBrain naming both | — |
| unset | malformed/blank | packaged default | packaged_default |
- The
[sync]table is never read. A[sync]table present in a config file is ignored entirely. get_saas_base_url()returns the env value orNone; it no longer raises.- Every hosted caller (auth login, token refresh, capability mint, admission preflight, tracker readiness, readiness nag) consumes the same resolved target; none may compute its own.
MISSING_HOST_CONFIGcan no longer occur; the readiness code for it is removed.