KB-37F4

S177 — Sprint 1 Round B Addendum — Cowork Consumption + Base Safety (2026-05-20)

11 min read Revision 1
s177larksprint1round-baddendumbindingcoworkmcpsafety

S177 — Sprint 1 Round B Addendum — Cowork Consumption + Base Safety

Status: Binding for Round B (when Round B is authorized). Date: 2026-05-20 Source: User-issued addendum, verbatim, on the Round A handoff thread. Pairs with:

  • s177-sprint1-command-review-package-2026-05-19.md (base spec)
  • s177-sprint1-implementation-checklist-2026-05-19.md
  • s177-architecture-design-2026-05-19-patch2.md
  • s177-sprint1-round-a-implementation-evidence-2026-05-19.md (Round A handoff)

This addendum supplements the Sprint 1 command-review package and PATCH2 design; it does not supersede them. Where this addendum sets a stricter or more specific rule than the base package, the addendum wins.

Round B is not yet authorized by virtue of this addendum existing. Execution requires a separate explicit GPT/User authorization on top of the existing gates.


1. Structured CLI output for Cowork/MCP consumption (binding)

All lark-tool records … commands implemented or stubbed in Round B must produce structured JSON when --json is used. This applies to:

  • success output
  • dry-run output
  • validation output
  • error output

Dry-run output must be machine-readable, not human-text-only.

Required dry-run shape (extensible — implementations may add fields, but must include these):

{
  "ok": true,
  "mode": "dry_run",
  "operation": "record.create",
  "base_key": "88-phai-cu-base-dem",
  "table_id": "tblXXX",
  "record_id": null,
  "would_write": {},
  "diff": [],
  "approval_status": "valid|exempt|missing|not_required",
  "safety_checks_passed": ["preflight", "policy", "approval"],
  "warnings": [],
  "operation_id": "uuid-or-deterministic-id",
  "audit_ref": null
}

For non-dry-run stubs in Round B (where real write execution is not authorized yet), the JSON output must clearly state that real write execution is not authorized (e.g. "ok": false, "error": true, "error_type": "NotAuthorized", "message": "real write execution not authorized in Round B; rerun with --dry-run").

2. Structured error output (binding)

When --json is used, errors must be JSON to stderr or stdout according to the existing CLI convention, and must be machine-readable.

Required error shape:

{
  "ok": false,
  "error": true,
  "exit_code": 1,
  "error_type": "SafetyViolation",
  "message": "approval missing",
  "reason": "approval_missing",
  "retry_after_seconds": null,
  "operation_id": "uuid-if-created",
  "audit_ref": "path-or-null",
  "details": {}
}

For Lark/API errors, include retry information when available:

{
  "ok": false,
  "error": true,
  "exit_code": 2,
  "error_type": "LarkAPIError",
  "message": "429 Too Many Requests",
  "retry_after_seconds": 5,
  "operation_id": "uuid",
  "audit_ref": "path-or-null"
}

Do not output raw PII in error JSON. PII fields surfaced in details must be redacted (metadata only — match audit pii convention from PATCH2 §P2-3).

3. Add records get (binding)

Round B must add the argparse surface and a service/read path for:

lark-tool records get <base-key> <table-id> <record-id>

Purpose:

  • Read one record by ID.
  • Support Cowork workflows.
  • Support future diff preview for update / delete.

Risk tier — safe read:

  • No approval required.
  • No backup.
  • No write.
  • No Lark mutation.

Mandatory invariants — even on a read path:

  • Validate base_key / table_id via Registry (no bare-token Base access).
  • Use the existing LarkReader or a sanctioned read path.
  • Produce structured JSON output per §1.
  • Audit lightly if the existing read audit convention supports it (log_call is already present).
  • Never bypass registry/config.

If the current LarkReader lacks get-by-ID, Round B may implement a no-write read method only if it uses the existing LarkCore read whitelist correctly. If this requires endpoint-whitelist changes:

  1. DISCOVER-FIRST against the current config/allowed_endpoints.yaml schema.
  2. Document the addition in the Round B evidence report (use the same {method, path, description} schema confirmed by OQ-9).
  3. Append to read:, not write:.

4. Approval-exempt-base test (binding)

Round B SafetyLayer tests must include a test that exercises the Base-đệm-exempt path:

  • An operation targeting a base listed in config/write-approvals.yaml :: approval_exempt_bases bypasses the Approval Check layer.
  • The same operation still runs every other applicable layer:
    • preflight
    • policy
    • dry-run / confirm
    • audit planned / result where applicable
    • lock if applicable
    • PII scan if applicable
    • no direct bypass to the operation call

Base đệm exemption means "approval not required", not "safety disabled". The test must assert the per-layer execution evidence (e.g. presence of planned-audit entry, presence of PII scan call, presence of lock acquisition).

5. WriteContext.source field (binding)

Add a source field to the operation context / WriteContext.

Required values (enum):

  • "cli"
  • "mcp"
  • "cron"
  • "api"

Round B default: source = "cli".

Future: the MCP adapter (Sprint 2) sets source = "mcp".

Audit entries must include both:

  • agent (from $LARK_AGENT / detection chain)
  • source (from call path)

Rationale: the operator must be able to distinguish whether an operation came from CLI, MCP/Cowork, cron, or API even when the same agent name is used.

Audit-schema implications (binding for Round B):

  • _MASK_SKIP_KEYS in lark_client/audit.py must be extended to include "source" so the value passes through unmasked.
  • The planned / result / emergency / orphan entry builders (already added in Round A) must include source in their output.
  • The 19-key mask-skip extension in Round A becomes a 20-key set in Round B.

6. Protect config/bases.yaml (binding)

Round B must not modify config/bases.yaml. This file contains production registry / app_token data and is immutable for Sprint 1.

Allowed:

  • read it
  • validate against it
  • use it in tests through fixtures/mocks
  • hard-assert Base đệm token via assert_buffer_base_token

Forbidden:

  • edit it
  • rewrite it
  • normalize it (no formatting, no key reordering, no comment changes)
  • add / remove bases
  • change role / staging / core metadata
  • change app_token values

If a Round B test needs altered registry data, the test must create a temp fixture under tmp_path (e.g. write a synthetic bases.yaml to a tmp path and pass config_dir=tmp_path to LarkCore / Registry), not modify the production config.

7. Partial progress rule for Round B (binding)

Round B is intentionally larger than Round A. If the agent completes a coherent subset but cannot finish all scope safely, it must report PARTIAL / BLOCKED rather than force completion.

Acceptable partial boundaries (in order):

  • B1: baseline fixes + request-leak cleanup
    • fix the pre-existing CLI smoke --json failures (path-pinned subprocess or remove stale /usr/local/bin/lark-tool)
    • refactor scripts/s179_probe.py to route through LarkReader instead of core._request(...) (and drop the scripts/ exclusion in the lint test)
    • WriteContext.source plumbing in lark_client/audit.py (_MASK_SKIP_KEYS += "source"; entry builders include source)
  • B2: ApprovalProvider + tests
    • YAML-backed registry against config/write-approvals.yaml
    • check_and_consume(approval_id, ctx, idempotency_key) with one-time-use lock (file-lock via fcntl.flock on the YAML directory)
    • approval_exempt_bases bypass with the test from §4 of this addendum
  • B3: SafetyLayer skeleton + tests
    • 8-layer orchestration per PATCH2 §P2-3
    • injectable dependencies (GPG=stub, PII=stub, API=mock) for unit-testability
    • emits structured WriteOutcome
    • audit_post_degraded path test
  • B4: Service + CLI records surface + tests
    • lark-tool records get|create|update|delete|batch_* argparse surface
    • stubs only for mutating commands — they enter SafetyLayer up to API call, then refuse with the §1/§2 structured "not authorized" JSON
    • records get is fully implemented per §3
    • all 5 commands produce structured JSON per §1, §2

If stopping partially:

  • Commit only the coherent passing subset, on the feature branch (no push).
  • Report exactly what passed (subset boundary B-n), what remains, and why.
  • Do not continue blindly.

8. Inheritance from Round A / Sprint 1 base

The following Round A + base-package constraints remain in force for Round B (this addendum does not override them):

  • No live Lark write executed (Round B's mutating commands stay as authorize-refusing stubs).
  • No production touched.
  • No deploy / push / merge / tag.
  • No MCP write enablement.
  • No new bot, no credential rotation, no secret printed.
  • No KB docs created outside knowledge/dev/lark/s177-controlled-crud-gateway/.
  • DISCOVER-FIRST is mandatory before any new file / config / module / secret reference.
  • LARK_BACKUP_GPG_PUBKEY remains a Sprint 1 full-PASS prerequisite (still absent in GSM as of 2026-05-20); Round B may scaffold the interface, but a GPG-touching slice ships only after the secret lands.
  • Round B may not self-advance to Sprint 2 (MCP adapter).

9. Required KB output for Round B

On completion (PASS, PARTIAL, or BLOCKED), Round B must upload:

knowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-round-b-implementation-evidence-2026-05-20.md

with the same fields the Round A evidence report has, plus:

  • Subset boundary reached (B1 / B2 / B3 / B4 / all)
  • Structured-output sample for each records … command added
  • Approval-exempt-base test transcript
  • source field appearance in a sample audit entry
  • Confirmation that config/bases.yaml was not modified (sha256 before/after)

And update the folder README pointer to the Round B evidence.


S177 Sprint 1 Round B addendum — binding when Round B is authorized.