S177 — Sprint 1 Round D Command Review Package — First Live Write on Base đệm (2026-05-20)
S177 — Sprint 1 Round D Command Review Package — First Live Write on Base đệm
Status: PASS — package ready for GPT/User review. No live Lark write executed by this package; one read-only records create --dry-run was issued against Base đệm to capture the exact audit-row shape that the live execution must reproduce. No production data touched, no MCP write enabled, no secret read, no commit, no push.
Date: 2026-05-20
Authored by: Claude Code (Opus 4.7, 1M context)
Folder: knowledge/dev/lark/s177-controlled-crud-gateway/
Pairs with:
s177-sprint1-round-c-implementation-evidence-2026-05-20.md(Round C baseline)s177-sprint1-round-b-implementation-evidence-2026-05-20.mds177-sprint1-round-b-addendum-2026-05-20.mds177-sprint1-command-review-package-2026-05-19.md(Sprint 1 base CRP)s177-architecture-design-2026-05-19-patch2.mds177-oq-decision-record-2026-05-19.md
1. Objective
Prepare the smallest safe live write test on Base đệm — records create --confirm against 88-phai-cu-base-dem, writing a single harmless marker string to one free-form Text field, surfacing created_record_id through the Cowork-shaped CLI JSON, and producing two audit rows (planned then success) with byte-identical operation_id / idempotency_key.
This package proposes the operation; it does NOT execute it. Execution requires (a) a separate authoring gate to wire a real api_caller (the CLI currently defaults to _round_b_refusing_api_caller) and (b) explicit sovereign approval of the exact command in §6.4.
2. Scope
In scope (planning only):
- One
record.createagainst Base đệm, tabletblaU7kxyPTNBSrR("Đơn hàng"), one field (Văn bản). - Exact command shape (CLI form,
LARK_AGENTinline,--agent,--dry-runthen--confirm). - The smallest possible source mutation to flip the CLI from refusing to real
LarkCore.write. - Expected stdout JSON, expected audit-row shape, expected lock + PII behavior.
- Rollback / cleanup policy.
Out of scope:
- Any write to production base
88-phai-cu(tokenYSIkb8PxOaNaozs2vwalOOcagkf). - MCP adapter (Sprint 2).
- Field / table / schema operations (Sprint 3/4).
records update/records delete/ batch ops on Base đệm. These requireLARK_BACKUP_GPG_PUBKEYto be provisioned in GSM (still absent — see §6.5).- Any execution. This package only specifies what the operator would run; the operator does not run it from this document.
3. SSOT consulted
knowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-round-c-implementation-evidence-2026-05-20.mdknowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-round-b-implementation-evidence-2026-05-20.mdknowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-command-review-package-2026-05-19.mdknowledge/dev/lark/s177-controlled-crud-gateway/s177-architecture-design-2026-05-19-patch2.md(read indirectly via Round B/C extracts)knowledge/dev/lark/s177-controlled-crud-gateway/s177-oq-decision-record-2026-05-19.mdknowledge/dev/lark/s177-controlled-crud-gateway/README.md(revision 12)
All six SSOTs present and readable. No STOP_AND_ESCALATE.
4. Mandatory live survey findings (2026-05-20)
All findings captured via ssh contabo from the laptop; no source mutation, no Lark write, no secret read.
| Item | Value |
|---|---|
| Repo root | /opt/incomex/lark-client/ |
| Branch | feat/s177-sprint1-round-a |
| HEAD | 2307265a11241f1ecc228ff92199c1f94b9c1483 (Round C) |
| Round C commit present? | YES — 2307265 S177 Sprint 1 Round C - replace stubs + pre-write readiness |
| Working tree | Clean modulo __pycache__ byproducts; no source modifications |
| Remote | NONE (git remote -v empty) — feat/s177-sprint1-round-a is local-only |
| Cold pytest | 134 passed, 5 skipped in 6.22s (env -u LARK_TEST_INTEGRATION .venv/bin/pytest -q) |
config/bases.yaml SHA256 |
c063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d (= Round C; unchanged) |
| Base đệm registry entry | key=88-phai-cu-base-dem, app_token=Nf2bb1ExXaYnlksgoyQl72GNgAc, role=staging |
| Production registry entry (forbidden target) | key=88-phai-cu, app_token=YSIkb8PxOaNaozs2vwalOOcagkf, role=core |
LARK_BACKUP_GPG_PUBKEY in GSM |
ABSENT (only LARK_APP_ID, LARK_APP_SECRET present — names only, no value read) |
LARK_BACKUP_GPG_PUBKEY_PATH env |
unset on the operator shell |
LARK_BACKUP_GPG_PUBKEY_FPR env |
unset on the operator shell |
LARK_AGENT env |
unset on the operator shell |
| Refusing api_caller present? | YES — lark_client/service.py:45 _round_b_refusing_api_caller raises NotAuthorizedInRoundB |
| Factory default | lark_client/factory.py:113 — api = api_caller if api_caller is not None else _round_b_refusing_api_caller |
| CLI default api_caller | cli/records.py::_build_service calls build_service(agent=...) with NO api_caller override → refusing path is the live default |
| Audit log primary sink | /var/log/lark-ops/<YYYYMMDD>.jsonl (e.g. 20260520.jsonl, root:root, 0755) |
| GPG binary on VPS | /usr/bin/gpg (GnuPG 2.4.4) — present, but no production public key wired |
4.1 Read-only dry-run probe captured live (this CRP)
A non-mutating --dry-run was issued to confirm preflight + audit-row shape on the exact base/table/payload proposed below. No Lark API call was made; the dry-run short-circuits at the api_call layer.
Command (verbatim, executed once at 2026-05-20T10:07:13 UTC):
.venv/bin/lark-tool --json records create 88-phai-cu-base-dem tblaU7kxyPTNBSrR \
--fields '{"Văn bản":"S177-RD-DRY-RUN-PROBE-PREFLIGHT"}' \
--dry-run --agent claude-code
Result: structured mode: dry_run JSON (full body reproduced in §6.8.1). pii.pii_redacted=false, pii.detector=["none"], field_hits=[]. Two audit rows written to /var/log/lark-ops/20260520.jsonl:
phase: planned,audit_pre_id=994a5851-…,operation_id=015bd6ee-…,idempotency_key=015bd6ee-…,agent=claude-code,source=cli,cmd=records.create,op=record.create,base_key=88-phai-cu-base-dem,table_id=tblaU7kxyPTNBSrR,dry_run=true,confirmed=false,is_buffer_base=true.phase: success, sameaudit_pre_id/operation_id/idempotency_key,outcome_status=dry_run,lark_response_meta={},request_id="",duration_ms=0,pii.pii_redacted=false.
The cli_start argv-log line confirmed argv-value masking: --fields {"Văn bản":"***"} (the value is replaced with *** in the cli_start record).
5. DISCOVER-FIRST — Base đệm tables and safe fields
Source: knowledge/dev/lark/probes/s179/base88_dem/ (laptop snapshot, 2026-04-12) + the dry-run probe in §4.1.
Base đệm contains two tables:
| Table name | table_id | Fields (count) |
|---|---|---|
| TTS | tblPQ6N79EeOmnTm |
7 — STT (AutoNumber primary), Ngày (DateTime), Phòng (SingleSelect NB4/Nhân/Thanh), Bảng 2 (DuplexLink→Đơn hàng), Bảng 1 2 (DuplexLink→TTS self), Văn bản (Text), Xác nhận (SingleSelect "Xác nhận") |
| Đơn hàng | tblaU7kxyPTNBSrR |
5 — STT (AutoNumber primary), Bảng 1 (DuplexLink→TTS), Ngày (DateTime), Phòng (SingleSelect NB4/Nhân/Thanh), Văn bản (Text) |
Field-safety analysis (binding for the proposed payload):
- AutoNumber
STT(primary) — system-assigned, do NOT set. - DuplexLink fields (
Bảng 1,Bảng 1 2,Bảng 2) — bidirectional; writing them mutates the linked record's reverse field. AVOID for a "smallest harmless test". - SingleSelect
Phòng(NB4 / Nhân / Thanh) — option names look team-coded, not a clear test marker. AVOID for cleanliness. - DateTime
Ngày— could be set, but irrelevant to a "presence-of-write" probe. AVOID to keep the payload minimal. - SingleSelect
Xác nhậnon TTS — one option ("Xác nhận") that carries business meaning. AVOID. - Text
Văn bản— free-form text on both tables. PREFERRED. Carries a marker string that we entirely control.
Table choice: tblaU7kxyPTNBSrR ("Đơn hàng"), NOT tblPQ6N79EeOmnTm ("TTS").
Rationale: config/pii-fields.yaml carries a wildcard registry entry base_key=88-phai-cu-base-dem, table_id=tblPQ6N79EeOmnTm, field_name="*", pii_types=[national_id_cccd, passport_vn, phone_vn, email, address]. ANY field write to TTS will trip detector=["registry"] even when the value is clearly non-PII (OQ-3 keeps the write proceeding, but pii_redacted=true is recorded). Đơn hàng (tblaU7kxyPTNBSrR) is NOT in the PII registry, so a write with a non-PII string yields the cleanest possible outcome: pii_redacted=false, detector=["none"], field_hits=[] — matches addendum-D1 "Preferred" outcome exactly, as verified by the live dry-run in §4.1.
No schema drift detected against the 2026-04-12 snapshot — the dry-run preflight resolved the base, accepted the Văn bản field name, and progressed through every Round C safety layer without error.
6. The package (17 items)
6.1 Objective
(Same as §1.)
6.2 Exact target
| Field | Value |
|---|---|
| base_key | 88-phai-cu-base-dem |
| app_token | Nf2bb1ExXaYnlksgoyQl72GNgAc (sha256 db48d36ed0b6b69116e15ed5d6a82bc1f5e3a907c83adfde3a73e0d3b3a59a6c) |
| Production token (forbidden) | YSIkb8PxOaNaozs2vwalOOcagkf — execution must abort if registry resolves to this |
| Role asserted | staging (must equal exactly; is_buffer_base=true derived) |
| table_id | tblaU7kxyPTNBSrR ("Đơn hàng") |
| Field payload (exact) | {"Văn bản": "S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>"} where <ISO8601-UTC> is the operator's wall clock at execution time and <uuid7> is a freshly generated UUID v4's first 7 hex chars (a test marker, NOT used for idempotency) |
| Operation | record.create |
Hash, not secret dump: the app_token sha256 above is provided so the reader can printf '%s' "$LARK_BASE_DEM_TOKEN" \| sha256sum to verify their environment matches without re-printing the token.
6.3 Why this table / payload is safe
- Đơn hàng is NOT in
pii-fields.yaml— registry detector cannot fire on this table. Văn bảnis plain Text — no enum coupling, no foreign-key coupling, no system-assigned semantics.- The marker string
"S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>"contains no digit runs ≥ 9 (ISO timestamp has at most 4 consecutive digits — the year), so none of the VN PII regexes match (CCCD = 12d, CMND = 9d, phone = 10d, passport = letter+7d, tax_code = 10–13d). The dry-run in §4.1 confirmedfield_hits=[]. - No DuplexLink is set, so no reverse record on the TTS table is mutated.
STT(AutoNumber primary) auto-fills — no primary-key conflict possible.is_buffer_base=true—approval_exempt_basescarries88-phai-cu-base-dem, so the SafetyLayer issues a syntheticApproval(id="EXEMPT")instead of consuming a real approval row.- Idempotent at the Lark side: Round C
lark-api-limits.yamlflipsrecords POST → client_token: true(OQ-8 citation…/record/create);LarkCore.writeinjectsctx.idempotency_key(UUID v4) into the body asclient_token. Replays during the same network blip will deduplicate server-side.
6.4 Exact proposed command (post-implementation-gate)
LARK_AGENT=claude-code \
/opt/incomex/lark-client/.venv/bin/lark-tool --json records create \
88-phai-cu-base-dem tblaU7kxyPTNBSrR \
--fields '{"Văn bản":"S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>"}' \
--confirm --agent claude-code
Notes:
LARK_AGENT=claude-codeis set inline as a defense-in-depth (the CLI also accepts--agent claude-code). Both produceaudit.agent="claude-code".--jsonselects structured-output mode; required for the post-execution audit verification in §6.16.- The placeholder
<ISO8601-UTC>and<uuid7>are substituted by the operator at execution time. Example concrete string:"S177-RD-LIVE-PROBE 2026-05-20T11:00:00Z 9f3c2a1".
This command is INERT today — see §6.5 / §D2: with HEAD = 2307265, the CLI hits _round_b_refusing_api_caller and returns NotAuthorizedInRoundB. The command becomes meaningful only after the source-mutation gate in §D2 is satisfied and reviewed.
6.5 Preconditions
Before the operator may execute §6.4, ALL of the following must hold. Each is independently verifiable:
| # | Precondition | Verification |
|---|---|---|
| P-1 | Round D source-mutation gate (§D2) is reviewed and merged on the feature branch | git log --oneline shows a commit titled S177 Sprint 1 Round D — wire real api_caller for Base đệm; cold pytest still green |
| P-2 | config/bases.yaml SHA256 still equals c063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d |
sha256sum config/bases.yaml |
| P-3 | LARK_AGENT=claude-code is set inline or exported on the operator shell |
printenv LARK_AGENT returns claude-code; OR command line carries LARK_AGENT=claude-code prefix |
| P-4 | GSM resolves LARK_APP_ID and LARK_APP_SECRET (already in place; required for any Lark call) |
gcloud secrets list --filter='name~LARK' --format='value(name)' shows both names; values not printed |
| P-5 | Registry resolves 88-phai-cu-base-dem → app_token starting with Nf2bb1Ex and role staging |
lark-tool registry show 88-phai-cu-base-dem (read-only) |
| P-6 | Sovereign written approval of the exact §6.4 command for one execution | Captured in the Round D execution-authorization message, NOT auto-derived from this CRP |
| P-7 | Network: contabo can reach open.larksuite.com over TCP/443 |
Tested separately (out of CRP scope; covered by existing reader tests) |
| P-8 | LARK_BACKUP_GPG_PUBKEY is NOT required for the proposed create (see §6.7) — but the operator must confirm by reading §6.7 |
6.6 Safety hard guards (binding for any Round D execution attempt)
The CLI / SafetyLayer already enforces all of the below; this section pins the invariants the operator must NOT bypass:
- G-1.
base_keyargument MUST equal88-phai-cu-base-dem. If any other key is passed, abort. - G-2.
Registry.get_by_key("88-phai-cu-base-dem").app_tokenMUST equalNf2bb1ExXaYnlksgoyQl72GNgAc. Theapp_tokenin flight MUST equal that value (LarkWriteService.resolve_baseis the single point of resolution; tampering requires editingconfig/bases.yaml— which is locked by sha256 invariant G-3). - G-3.
sha256sum config/bases.yamlMUST equalc063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5dimmediately before and immediately after execution. Production token MUST be rejected (the registry assertion + theis_buffer_base=truederived flag together exclude it). - G-4.
--confirmMUST be present (Round B addendum §2 — mutation refuses without--dry-runor--confirm). - G-5.
LARK_AGENTMUST resolve toclaude-code(or another explicitly approved value — D4). Default fallbacks ($USER@$HOSTNAME,unknown-$PID) MUST NOT be allowed for an authorized write. - G-6. The api_caller wired by Round D MUST refuse if
ctx.base_key != "88-phai-cu-base-dem"ANDctx.is_buffer_base != True. This is a defense-in-depth check that the Round D implementation gate (§D2) must include. - G-7. No edits to
config/bases.yaml,config/write-approvals.yaml, orconfig/allowed_endpoints.yamlbetween dry-run and live execution. SHAs MUST be pinned.
6.7 Whether GPG is required
Not required for the proposed record.create.
Per lark_client/safety.py (Round B addendum §C5 + Round C §7.1), the SafetyLayer invokes gpg_backup(ctx, payload) only when ctx.op ∈ {record.update, record.delete, record.batch_update, record.batch_delete}. record.create and record.batch_create skip the backup layer entirely — there is no prior state to back up.
Operationally:
- With
LARK_BACKUP_GPG_PUBKEY_PATH/_FPRunset,factory.build_servicewires the Round B stub backup. For create, the stub is never called. - Round C
tests/test_round_c_scenarios.py::test_scenario_create_on_buffer_base_emits_created_record_idalready proves thatgpg_backupis NOT invoked forrecord.create.
Consequence: Round D step 1 (create) does not block on GSM provisioning. Steps 2+ (update/delete) DO block on it (see §6.12 Option C).
6.8 Expected output JSON
6.8.1 --dry-run (already verified live, §4.1)
{
"ok": true,
"mode": "dry_run",
"operation": "record.create",
"base_key": "88-phai-cu-base-dem",
"table_id": "tblaU7kxyPTNBSrR",
"record_id": null,
"would_write": {"fields": {"Văn bản": "S177-RD-DRY-RUN-PROBE-PREFLIGHT"}},
"diff": [],
"approval_status": "exempt",
"safety_checks_passed": [
"preflight", "dry_run_or_confirm", "approval",
"audit_planned", "lock", "pii_scan"
],
"warnings": [],
"operation_id": "<uuid-v4>",
"audit_ref": "<uuid-v4>",
"pii": {
"pii_redacted": false,
"redaction_types": [],
"redacted_fields_count": 0,
"detector": ["none"],
"field_hits": []
}
}
6.8.2 --confirm (expected on first live execution)
{
"ok": true,
"mode": "live",
"operation": "record.create",
"base_key": "88-phai-cu-base-dem",
"table_id": "tblaU7kxyPTNBSrR",
"record_id": null,
"created_record_id": "rec<≥10 alphanumeric>",
"data": {"code": 0, "msg": "ok", "data": {"record": {"record_id": "rec…", "fields": {"Văn bản": "S177-RD-LIVE-PROBE …"}}}},
"approval_status": "exempt",
"safety_checks_passed": [
"preflight", "dry_run_or_confirm", "approval",
"audit_planned", "lock", "pii_scan", "api_call"
],
"warnings": [],
"operation_id": "<uuid-v4>",
"audit_ref": "<uuid-v4>",
"pii": {
"pii_redacted": false,
"redaction_types": [],
"redacted_fields_count": 0,
"detector": ["none"],
"field_hits": []
}
}
The single most important field is created_record_id — the Round D evidence report MUST quote it verbatim.
6.9 Expected audit entries
Path: /var/log/lark-ops/<YYYYMMDD>.jsonl (daily-sharded, append-only, root:root, 0755 dir). Today's file is /var/log/lark-ops/20260520.jsonl.
Two new entries (in this order) must be appended for a single live execution:
Entry 1 — phase: planned
{
"ts": "<ISO8601 with µs and tz>",
"phase": "planned",
"audit_pre_id": "<uuid-v4>",
"operation_id": "<uuid-v4>",
"idempotency_key": "<same uuid-v4 as operation_id>",
"agent": "claude-code",
"source": "cli",
"cmd": "records.create",
"op": "record.create",
"base_key": "88-phai-cu-base-dem",
"table_id": "tblaU7kxyPTNBSrR",
"targets": [],
"target_count": 0,
"approval_id": "",
"backup_ref": null,
"dry_run": false,
"confirmed": true,
"is_buffer_base": true
}
Entry 2 — phase: success
{
"ts": "<ISO8601 with µs and tz, > Entry 1 ts>",
"phase": "success",
"audit_pre_id": "<same as Entry 1>",
"operation_id": "<same as Entry 1>",
"idempotency_key": "<same as Entry 1>",
"agent": "claude-code",
"source": "cli",
"cmd": "records.create",
"op": "record.create",
"base_key": "88-phai-cu-base-dem",
"table_id": "tblaU7kxyPTNBSrR",
"request_id": "<Lark X-Tt-Logid or response request_id>",
"lark_response_meta": {"code": 0, "msg": "ok", "http_status": 200, "request_id": "<string>", "server_time": "<string>"},
"duration_ms": <int>,
"pii": {"pii_redacted": false, "redaction_types": [], "redacted_fields_count": 0, "detector": ["none"], "field_hits": []},
"outcome_status": "success",
"error": null
}
The cli_start line that precedes Entry 1 must show argv-value-masked fields: --fields {"Văn bản":"***"} (already verified in §4.1).
6.10 Expected lock behavior
Per lark_client/locks.py (Round C §9.2):
record.create→ table-level lock at<LARK_LOCK_DIR>/88-phai-cu-base-dem/tblaU7kxyPTNBSrR.lock.LARK_LOCK_DIRenv default →/var/lock/lark-ops/(lazily created by the acquirer; root:root expected).- Acquisition is
fcntl.flock(LOCK_EX | LOCK_NB); concurrent contention raisesSafetyViolation(reason="lock_held")immediately. - The lock file is not deleted after release (advisory
fcntlis sufficient; on-disk file kept for forensics).
The Round D evidence report MUST capture the lockfile path and confirm it exists after execution.
6.11 Expected PII scanner behavior
Inputs (per §6.2 payload): {"Văn bản": "S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>"}.
| Detector | Result | Rationale |
|---|---|---|
registry |
NO HIT | Đơn hàng (tblaU7kxyPTNBSrR) is NOT in config/pii-fields.yaml (the wildcard entry scopes specifically to TTS tblPQ6N79EeOmnTm). |
pattern — national_id_cccd (12d isolated) |
NO HIT | Marker string has at most 4 consecutive digits (year). |
pattern — national_id_cmnd (9d) |
NO HIT | (same) |
pattern — passport_vn ([BCGN]\d{7}) |
NO HIT | No letter+7-digit sequence. |
pattern — phone_vn (03/05/07/08/09 or +84) |
NO HIT | (same) |
pattern — email |
NO HIT | No @. |
pattern — bank_account (10–19d) |
NO HIT | (same) |
pattern — tax_code_vn |
NO HIT | (same) |
Expected scanner output (verified in the live dry-run in §4.1):
{
"pii_redacted": false,
"redaction_types": [],
"redacted_fields_count": 0,
"detector": ["none"],
"field_hits": []
}
No PII metadata path needs to be exercised for OQ-3 (the layer still runs; it just records none). Even if it had matched, OQ-3 says the write proceeds and only metadata is logged — never raw values.
6.12 Rollback / cleanup
Per addendum-D3 — LARK_BACKUP_GPG_PUBKEY is absent; therefore records delete from the Gateway is not available without an out-of-band cleanup step.
Chosen policy: Option A (leave-and-mark).
- Create one harmless record on
88-phai-cu-base-dem :: tblaU7kxyPTNBSrR. - The marker string
"S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>"clearly identifies it as test data. - Record the returned
created_record_idin the Round D evidence report. - Leave it in the Base. Do not attempt to delete from the Gateway in this round.
- Delete is a separately authorized later gate, opened only after
LARK_BACKUP_GPG_PUBKEYis provisioned in GSM (then the Gateway can routerecords delete --confirmthrough the GPG-backed safety chain).
Option B (manual delete via Lark web) is permitted only with explicit Huyên approval and explicit notation that the deletion bypassed the Gateway. Not the recommended path.
Option C (provision GPG then Gateway delete) is the long-term-correct path; it is out of scope for this CRP — it is the immediate next macro after Round D step 1.
6.13 PASS criteria (for the live execution, post-implementation gate)
After the proposed §6.4 command runs once, the Round D execution PASSES if and only if ALL of:
- CLI exit code 0.
- CLI JSON has
ok: true,mode: "live",operation: "record.create",base_key: "88-phai-cu-base-dem",table_id: "tblaU7kxyPTNBSrR". - CLI JSON has a non-empty
created_record_idmatching^rec[A-Za-z0-9]{8,}$. - CLI JSON
safety_checks_passedcontains exactly the 7 items in §6.8.2 (including"api_call"). - CLI JSON
pii.pii_redacted === false,pii.detector === ["none"],pii.field_hits === []. /var/log/lark-ops/<YYYYMMDD>.jsonlcontains a newphase: plannedrow matching §6.9 Entry 1./var/log/lark-ops/<YYYYMMDD>.jsonlcontains a newphase: successrow matching §6.9 Entry 2 withoutcome_status: "success"and a non-emptyrequest_id.- The
operation_idandidempotency_keyare byte-identical across the two audit rows AND match the CLI JSONoperation_idand theclient_tokeninjected into the Lark POST body. lark_response_metakeys are a subset of{code, msg, http_status, request_id, server_time}— no raw record fields echoed back into audit.sha256sum config/bases.yamlstill equalsc063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d.- No raw value of
Văn bảnever appears in stdout, audit, KB report, or anywhere outside the Lark response body itself (argv masking already verified in §4.1). - The lockfile at
/var/lock/lark-ops/88-phai-cu-base-dem/tblaU7kxyPTNBSrR.lockexists andfcntlis no longer holding it (i.e., the next invocation could re-acquire).
6.14 BLOCKED criteria (for this CRP)
Report this CRP as BLOCKED if any of:
- Round C HEAD is NOT
2307265a11241f1ecc228ff92199c1f94b9c1483(live survey would invalidate). Verified PASS in §4. config/bases.yamlSHA256 has drifted. Verified PASS in §4.88-phai-cu-base-demno longer in the registry. Verified PASS in §4.- Cold pytest is not green. Verified PASS in §4 (134 passed / 5 skipped).
- Any safe non-PII field cannot be identified on Base đệm. Verified PASS in §5 (Đơn hàng :: Văn bản).
- Source mutation to flip api_caller is required AND a separate gate cannot be opened. PASS — gate proposed below (§D2).
This CRP reports PASS on all of the above.
For the execution itself (a later step, NOT this CRP), the operator must report BLOCKED if any of:
- Registry resolves to the production token
YSIkb8Px…or any non-Base-đệm key. LARK_AGENTresolves to anything other than the explicitly approved value (default fallback → BLOCKED).config/bases.yamlSHA256 has drifted between dry-run and confirm.--confirmis missing OR--dry-runis also present (mutually exclusive intent).- Round D source-mutation gate (§D2) is not yet merged.
- Network to
open.larksuite.comis not reachable.
6.15 FAIL criteria (for the live execution, post-implementation gate)
The Round D execution FAILS if any of:
- CLI exits non-zero with
error_type ∈ {SafetyViolation, NotAuthorizedInRoundB, UnknownBaseError, EndpointNotAllowed, ApprovalError}(the SafetyLayer caught a violation — this means the implementation gate §D2 is incomplete or the preconditions in §6.5 were not honored). LarkAPIErrorwithcode != 0(Lark rejected the write — must surfacecode,msg,request_idin the structured-error JSON; the test record was NOT created and there is nothing to clean up).- Two
phase: plannedrows but nophase: successrow appears within 30 s (audit-post degraded → look forphase: emergency_post_auditin/var/log/lark-ops/EMERGENCY/<YYYYMMDD>/). created_record_idis empty or absent in the JSON despiteok: true.- Any of the PASS-criteria-violations in §6.13 inverted.
A FAIL is recoverable — the create is idempotent (server-side client_token deduplication when supported, or absence of the record when rejected). Re-run is permitted ONLY after the cause is diagnosed AND a fresh sovereign approval is granted.
6.16 Exact evidence report expected after execution
If and when the §6.4 command is later executed under sovereign authorization, the operator MUST upload a Round D evidence report at:
knowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-round-d-live-write-evidence-2026-05-20.md
Required sections:
- Top-line result (PASS / FAIL / BLOCKED).
- Repo / branch / HEAD (post-D2-mutation commit pinned).
- Pre-execution live survey (sha of bases.yaml, env state, registry resolution).
- Exact command run (with timestamp, exit code, full stdout JSON quoted).
created_record_idquoted verbatim.- Audit rows quoted verbatim (
phase: planned+phase: success) with timestamps. operation_id/idempotency_keycorrelation table.- Lockfile path + post-execution existence proof.
sha256sum config/bases.yamlbefore and after — must matchc063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d.- Cold pytest re-run post-execution (must still be 134 passed / 5 skipped or whatever Round-D-baseline replaces it).
- PII scanner output (must be the §6.11 "none" shape; if not, explain).
- Confirmation of forbidden-action negatives (no push, no MCP, no production touched, no secret printed, no source mutation beyond the §D2 gate, no bases.yaml edit).
- STOP route — explicit hand-back to GPT/User.
6.17 Forbidden actions
For Round D step 1 (live create) and for this CRP itself:
- DO NOT write to any base other than
88-phai-cu-base-dem. - DO NOT write to TTS table (
tblPQ6N79EeOmnTm) — Đơn hàng (tblaU7kxyPTNBSrR) is the chosen target. - DO NOT run
records update,records delete, or anyrecords batch_*against Base đệm in Round D step 1. - DO NOT rotate / read / print any GSM secret.
- DO NOT edit
config/bases.yaml,config/write-approvals.yaml,config/allowed_endpoints.yaml,config/lark-api-limits.yaml, orconfig/pii-fields.yaml. - DO NOT
git push,git merge,git tag, or rebase. Round D mutations remain onfeat/s177-sprint1-round-alocal-only. - DO NOT enable MCP write tools or any new MCP server.
- DO NOT deploy, restart, or rebuild any container or service.
- DO NOT create a new bot or rotate
cli_a785d634437a502f. - DO NOT delete the test record from the Gateway (Option A leaves it).
- DO NOT create KB docs outside
knowledge/dev/lark/s177-controlled-crud-gateway/. - DO NOT self-advance to Round D step 2 (update/delete) or Sprint 2 (MCP adapter) without separate sovereign authorization.
- DO NOT execute §6.4 from this CRP. This document is a proposal.
7. Round D CRP Addendum
D1. Expected PII scanner behavior for the proposed payload
Fields used: Văn bản only (Text). Value: marker string "S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>".
Văn bảnis NOT inconfig/pii-fields.yaml(registry entries cover Base 88 production fields plus a wildcard scoped to TTStblPQ6N79EeOmnTm; Đơn hàngtblaU7kxyPTNBSrRhas no entry).- The marker value matches none of the Vietnamese PII regex patterns (no 9/12-digit runs, no
[BCGN]\d{7}, no@, no03|05|07|08|09|+84-prefixed 10-digit). - Expected scanner output:
pii_redacted: false, redaction_types: [], redacted_fields_count: 0, detector: ["none"], field_hits: []. Verified live in §4.1 dry-run.
If PII somehow surfaces (e.g., operator substitutes the marker string for a real value before execution), OQ-3 governs: the write still proceeds, only metadata is logged, raw values never appear in stdout / audit / KB. But that path is explicitly NOT taken here — the marker is opaque.
D2. Factory wiring from refusing stub to real LarkCore.write
Source mutation IS required. Today (HEAD 2307265):
cli/records.py:251 _build_service(args)callsfrom lark_client.factory import build_service; return build_service(agent=getattr(args, "agent", None))— and passes NOapi_caller.lark_client/factory.py:113then defaults to_round_b_refusing_api_caller.lark_client/service.py:45-47 _round_b_refusing_api_caller(ctx)raisesNotAuthorizedInRoundB(ctx.op)for ALL mutating ops.
There is NO env var, CLI flag, or config toggle to flip this. The injection hook args._service_override exists only for in-process tests.
The exact pre-execution gate for Round D step 1:
Gate D-IMPL (separate authoring macro, separately reviewed, separately approved):
Add a new function _real_lark_api_caller(core: LarkCore, *, registry: Registry, limits_path: Path) -> ApiCaller to lark_client/factory.py. It returns a closure (ctx: WriteContext) -> dict that:
- Hard-guards Base-đệm-only at the api_caller boundary:
if ctx.base_key != "88-phai-cu-base-dem" or not ctx.is_buffer_base: raise SafetyViolation("base_not_authorized_for_round_d", …). This is a defense-in-depth guard ON TOP of the existing whitelist + registry. - Resolves the table-template endpoint for
ctx.op(initially onlyrecord.create→POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records). - Looks up
client_token_supportedfromconfig/lark-api-limits.yaml::write_endpoint_options(already citation-backed by Round C OQ-8:POST /records → client_token: true). - Calls
core.write(method=..., endpoint=..., json_data=ctx.payload, idempotency_key=ctx.idempotency_key, client_token_supported=<bool>, _audit_cmd=ctx.op). - Returns the dict.
Then thread an opt-in flag through cli/records.py::_build_service — preferred shape: a new top-level CLI flag --enable-live-write (defaults to OFF) which causes _build_service to pass api_caller=_real_lark_api_caller(core, …) to build_service. Without the flag, the refusing default stays.
Estimated size of the D-IMPL patch: ~60-90 net new LOC across factory.py + cli/records.py + 2-3 new unit tests in tests/test_round_d_wiring.py (mock LarkCore + assert the closure routes to core.write with the expected kwargs).
Round D step 1 (live execution of §6.4) MUST NOT happen until D-IMPL is reviewed and merged. This CRP does NOT itself open D-IMPL — D-IMPL is the next macro after this CRP is accepted.
Verification that the live invocation will route to real LarkCore.write:
- Static:
grep -n "_real_lark_api_caller\|core.write" lark_client/factory.py cli/records.pyafter D-IMPL must show the closure is wired conditionally on--enable-live-write. - Runtime: a wired dry-run (D-IMPL merged +
--enable-live-write --dry-run) MUST still short-circuit before the api_caller — confirms the dry-run gate sits ABOVE api_caller as today. - Runtime: a wired confirm (D-IMPL merged +
--enable-live-write --confirm) on a base OTHER than88-phai-cu-base-demMUST raise the newSafetyViolation("base_not_authorized_for_round_d"). This is the Base-đệm-only guard.
Preventing accidental production write: the three defenses are stacked:
- Registry: production token is at key
88-phai-cu, NOT88-phai-cu-base-dem. Resolving by the wrong key returns the wrong token but trips the next layer. - SafetyLayer:
ctx.is_buffer_baseis derived fromregistry.get_by_key(base_key).role == "staging";88-phai-cuhasrole: core→is_buffer_base=false. - The new D-IMPL api_caller boundary guard explicitly checks
ctx.base_key == "88-phai-cu-base-dem" AND ctx.is_buffer_base == True.
D3. Rollback / cleanup must be realistic
Per addendum-D3 binding choice: Option A (leave-and-mark) is selected.
- Create one harmless test record (this CRP proposes exactly one).
- Leave it on Base đệm.
- The marker string
"S177-RD-LIVE-PROBE <ISO8601-UTC> <uuid7>"self-identifies as test data. created_record_idis recorded in the Round D evidence report.- The record persists until
LARK_BACKUP_GPG_PUBKEYis provisioned, at which point a separately authorized Gatewayrecords delete --confirmcan remove it through the full safety chain (including GPG-encrypted pre-write backup of the row being deleted).
Option B (manual delete via Lark web) is permitted only with explicit Huyên approval, recorded out-of-band. Option C (provision GPG then Gateway delete) is the recommended long-term path and is the next macro after Round D step 1.
This CRP does not promise Gateway-delete cleanup, because LARK_BACKUP_GPG_PUBKEY is absent today.
D4. LARK_AGENT must be explicit
Live survey (§4):
printenv LARK_AGENTon the operator shell returns nothing.- Default fallback in
lark_client/audit.py::_detect_agentis$USER@$HOSTNAME→root@unknownon contabo SSH. NOT acceptable for an authorized live write.
The §6.4 command sets LARK_AGENT=claude-code inline (defense-in-depth — even if the operator's shell loses the env var, the CLI inherits it). The CLI ALSO accepts --agent claude-code as a redundant override.
Expected audit fields:
"agent": "claude-code",
"source": "cli"
source is hard-coded "cli" inside cli/records.py::_build_ctx (verified in §4.1 dry-run audit). It cannot be MCP/cron/api from this invocation path.
If printenv LARK_AGENT returns empty AND the inline assignment is omitted AND --agent is omitted → BLOCKED. The Round D execution-side checklist must verify that audit.agent is "claude-code" and not the default fallback.
D5. Post-execution audit verification
Path: /var/log/lark-ops/<YYYYMMDD>.jsonl (daily-sharded; today = /var/log/lark-ops/20260520.jsonl).
Locate the two rows by matching audit_pre_id against the CLI JSON's audit_ref. Concrete one-liner:
audit_ref="<from CLI JSON>"
grep -F "\"audit_pre_id\": \"$audit_ref\"" /var/log/lark-ops/$(date -u +%Y%m%d).jsonl
Required checks (each MUST pass; any failure → execution FAIL):
phase: plannedrow exists. Schema-match §6.9 Entry 1.phase: successrow exists. Schema-match §6.9 Entry 2.- Correlation:
phase: planned :: audit_pre_id == phase: success :: audit_pre_id.phase: planned :: operation_id == phase: success :: operation_id. Both equalphase: planned :: idempotency_key == phase: success :: idempotency_key. created_record_idcorrelation: CLI JSONcreated_record_idMUST equal therecord_idextracted from the Lark response body (and from thephase: success :: lark_response_metaifrequest_idis the only key — i.e.,created_record_idneed not appear inlark_response_meta, but it MUST appear in the original Lark response stored indataof the CLI JSON).- PII metadata present in
phase: success, withpii_redacted: falsefor this clean payload. (For other payloads, OQ-3: metadata always present; never raw values.) - Required fields in
phase: success:agent="claude-code",source="cli",base_key="88-phai-cu-base-dem",table_id="tblaU7kxyPTNBSrR",op="record.create",idempotency_key=<uuid-v4>. - No raw PII anywhere:
grep -E "<\bcandidate raw values\b>"over stdout JSON + the two audit rows + the Round D evidence markdown MUST return no matches. (For the proposed payload there are no PII candidates — but the operator must still run the check.) - Failure-of-success-row protocol: if
phase: plannedexists butphase: successis missing AFTER 30 s, look for a sibling row at/var/log/lark-ops/EMERGENCY/<YYYYMMDD>/<ts>-<idempotency_key>.jsonwithphase: emergency_post_audit, error: audit_post_degraded. The CLI JSON in that case would carryerror: "audit_post_degraded"butstatus: "success"— the Lark write happened but the primary audit-result sink failed; the emergency sink is the authoritative success record.
If audit verification cannot be performed (e.g., /var/log/lark-ops/ is unreadable) → BLOCKED. The execution-side checklist must include a ls -la /var/log/lark-ops/ precheck and a post-execution tail -2 /var/log/lark-ops/<YYYYMMDD>.jsonl capture.
8. PASS report for this CRP
This package reports PASS because:
- Round D CRP authored at the canonical KB path:
knowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-round-d-command-review-package-2026-05-20.md. - README updated to record Round D as filed.
- Target base / table / payload justified by §4 + §5 + §4.1 live dry-run.
- The proposed command in §6.4 is exact and copyable.
- Rollback / cleanup plan is clear (§6.12 Option A).
- No live Lark write occurred — the dry-run in §4.1 is a non-mutating preflight that short-circuits before the api_call layer; no
phase: successwithoutcome_status: "success"was emitted, onlyoutcome_status: "dry_run". - No forbidden action occurred — no push, no merge, no tag, no MCP write, no production touch, no secret printed, no source mutation, no
config/bases.yamledit, no other KB folder touched.
Open blockers before execution:
- Round D source-mutation gate (D-IMPL per §D2) must be opened, reviewed, merged.
- Explicit sovereign approval of the §6.4 command must be issued in a separate authorization message.
LARK_BACKUP_GPG_PUBKEYprovisioning is NOT a blocker for step 1 (create skips backup), but IS a blocker for any later update/delete.
Authorized next macros (in order):
- D-IMPL — authoring + tests for
_real_lark_api_caller(+--enable-live-writeflag). - D-EXEC — execution of §6.4 (separate explicit authorization required).
- D-EVIDENCE — upload
s177-sprint1-round-d-live-write-evidence-2026-05-20.mdper §6.16. - GSM provisioning of
LARK_BACKUP_GPG_PUBKEY→ enables Round D step 2 (update/delete on Base đệm) and Round D cleanup (Gateway delete of the leave-and-mark record).
9. STOP
Round D CRP authoring complete. STOP — route back to GPT/User for execution-authorization decision. Do not self-advance to D-IMPL or D-EXEC.