KB-30A4

S177 — Sprint 1 Round C Implementation Evidence (2026-05-20)

28 min read Revision 1
s177larksprint1round-cimplementationevidencepassoq8-closed

S177 — Sprint 1 Round C Implementation Evidence

Status: PASS — Round C (B+C all in.) Date: 2026-05-20 Authored by: Claude Code (Opus 4.7, 1M context) Pairs with:

  • s177-sprint1-round-b-implementation-evidence-2026-05-20.md (Round B baseline)
  • s177-sprint1-round-b-addendum-2026-05-20.md
  • s177-sprint1-round-a-implementation-evidence-2026-05-19.md
  • s177-sprint1-command-review-package-2026-05-19.md
  • s177-architecture-design-2026-05-19-patch2.md
  • s177-oq-decision-record-2026-05-19.md

1. Top-line result

  • PASS for every Round C MUST clause (addendum §C7): PII registry seeded (29 entries), VN PII regex patterns shipped, WriteOutcome carries created/affected record IDs, no raw PII in logs/output, no Lark write executed.
  • PASS for SHOULD clauses: table-level lock for batch operations; GPG mock + real-key test strategy; end-user scenario tests for Cowork-facing flows.
  • OQ-8 resolved via authoritative Lark Open Platform doc citation. config/lark-api-limits.yaml updated accordingly (see §6).
  • GPG backup backend implemented (encrypt-only, isolated GNUPGHOME, sidecar metadata). Real GPG end-to-end test passes with a transient RSA-2048 key. Production GSM secret still absent (see §13.2).
  • PII scanner implemented (registry + Vietnamese pattern). Metadata-only output. Egress-block raises SafetyViolation(pii_egress_blocked).
  • File-lock acquirer implemented (fcntl.flock LOCK_EX|LOCK_NB). Record-level for update/delete, table-level for batch and create. Cross-process contention test passes.
  • Round C single commit on the existing feature branch.
    • Base commit: 1cf7901 (Round B4)
    • New commit: 2307265 (S177 Sprint 1 Round C — replace stubs + pre-write readiness)
  • Cold pytest result: 134 passed, 5 skipped, 0 failed. (5 skipped = pre-existing integration tests gated on LARK_TEST_INTEGRATION=1.)
  • No live Lark API call. No production touched. No Base đệm write. No deploy. No push/merge/tag. No MCP write enablement. No new bot. No credential rotation. No secret printed.
  • config/bases.yaml SHA256 unchangedc063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d before and after Round C.

2. Repo / branch / HEAD

Item Value
Repo root /opt/incomex/lark-client/
Branch feat/s177-sprint1-round-a (continued from Round B; NOT pushed)
Base commit (pre-C) 1cf7901S177 Sprint 1 Round B4 — LarkWriteService + CLI records surface
New commit (Round C) 2307265S177 Sprint 1 Round C - replace stubs + pre-write readiness
Remote ops none — no push, no merge, no tag

3. Live survey findings

Survey ran before any source mutation (Bash via ssh contabo).

  • git status showed clean feature-branch state except __pycache__ byproducts; no surprise modifications.
  • HEAD at 1cf7901 matched the Round B evidence.
  • All Round B files present and unmodified pre-C:
    • lark_client/{audit,approval,safety,service,core,reader,registry,exceptions}.py
    • cli/{lark_tool,records}.py
    • config/{bases,allowed_endpoints,lark-api-limits,write-approvals,pii-fields}.yaml
  • lark_client/gpg_backup.py, lark_client/pii.py, lark_client/locks.py, lark_client/factory.py did not exist → safe to create.
  • gpg binary present on VPS (/usr/bin/gpg, GnuPG 2.4.4, libgcrypt 1.10.3).
  • /var/lock/lark-ops/ did not exist → tests use tmp_path; production wiring creates the directory lazily under the FileLockAcquirer.
  • gcloud secrets list --filter='name~LARK' (by name only — no values read): LARK_APP_ID, LARK_APP_SECRET. LARK_BACKUP_GPG_PUBKEY still absent.
  • Round B test suite re-run cold: 82 passed, 5 skipped before Round C edits.

4. DISCOVER-FIRST findings

Module/config Round C action
lark_client/safety.py Extended (WriteOutcome ID surface; lark_response_meta whitelisting; response parsing helpers).
lark_client/audit.py Untouched (already correct after Round B).
lark_client/approval.py Untouched (Round B implementation already complete).
lark_client/service.py Untouched (factory wraps it).
cli/records.py Patched (_build_service delegates to factory; emitters surface IDs + PII).
cli/lark_tool.py Untouched (records dispatch unchanged).
config/bases.yaml Untouched (binding §6 of Round B addendum, retained).
config/allowed_endpoints.yaml Untouched (no new endpoints needed for Round C).
config/write-approvals.yaml Untouched (exempt list and approvals registry already correct).
config/lark-api-limits.yaml Modified per OQ-8 citation.
config/pii-fields.yaml Seeded (29 entries) from KB schema.
lark_client/gpg_backup.py New (Round C addendum §C5).
lark_client/pii.py New (Round C addendum §C1+C2).
lark_client/locks.py New (Round C addendum §C3).
lark_client/factory.py New (Round C addendum §E).
tests/test_gpg_backup.py New (10 tests).
tests/test_pii.py New (18 tests).
tests/test_locks.py New (16 tests).
tests/test_round_c_scenarios.py New (8 tests, addendum §C6 scenarios).
tests/test_configs_parse.py Modified (Round C seed + OQ-8 citation assertions).

5. git diff --stat (Round C only)

 lark-client/cli/records.py                  |  72 +++--
 lark-client/config/lark-api-limits.yaml     |  32 +-
 lark-client/config/pii-fields.yaml          | 237 +++++++++++++-
 lark-client/lark_client/factory.py          | 131 ++++++++
 lark-client/lark_client/gpg_backup.py       | 335 ++++++++++++++++++++
 lark-client/lark_client/locks.py            | 213 +++++++++++++
 lark-client/lark_client/pii.py              | 268 ++++++++++++++++
 lark-client/lark_client/safety.py           | 212 +++++++++----
 lark-client/tests/test_configs_parse.py     |  32 +-
 lark-client/tests/test_gpg_backup.py        | 314 +++++++++++++++++++
 lark-client/tests/test_locks.py             | 189 +++++++++++
 lark-client/tests/test_pii.py               | 185 +++++++++++
 lark-client/tests/test_round_c_scenarios.py | 465 ++++++++++++++++++++++++++++
 13 files changed, 2584 insertions(+), 101 deletions(-)

SHA256 of new/modified modules:

File sha256
lark_client/safety.py 82373d86e1960bc7901c625694c99002cdd3917668e8561b8265d92176e8fa79
lark_client/gpg_backup.py 415cf04fe5e0e2f3fdf64517f6247e8c90e8fb2f601552fb54c2ce8110055bd4
lark_client/pii.py 7ce4c9169eac421f66407edaf850381e92ef4428ee910b0d696a86479d0fe42a
lark_client/locks.py 2c593c798f248c89ce8be62d64072598018b0e6be5f4ec0d40d74b59f0f3cd07
lark_client/factory.py 4b1718aaaa214a5b94e9b160bbb73932ae7b57be6b687ddb9401b32418ed5804
cli/records.py feaf12cb18831e2f16d0e5b90d951248b406e845214328e51f45d559720ca33e
config/pii-fields.yaml ba26ca63b56058e851e39b4fe450bd376808e8759afd0cfba6dd6a53df0910fb
config/lark-api-limits.yaml 8e5a4332ee63ae8e5c66cadb6e9962c61ad1aab517fb070384b3705cfb67d603

6. OQ-8 status and citations

Status: RESOLVED via authoritative Lark Open Platform documentation citation (2026-05-20). All six record endpoints were verified individually.

Endpoint client_token Source URL
POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records YES open.larksuite.com/document/server-docs/docs/bitable-v1/app-table-record/create
POST .../records/batch_create YES …/app-table-record/batch_create
PUT .../records/{record_id} (update) YES …/app-table-record/update
DELETE .../records/{record_id} (delete) NO …/app-table-record/delete (no query params documented)
POST .../records/batch_update NO …/app-table-record/batch_update (only user_id_type + ignore_consistency_check)
POST .../records/batch_delete NO …/app-table-record/batch_delete (no query params documented)

Quoted text for the three positive citations (from …/record/create, …/record/batch_create, …/record/update): "格式为标准的 uuidv4,操作的唯一标识,用于幂等的进行更新操作。" (idempotent token of standard UUID v4).

config/lark-api-limits.yaml updated accordingly:

  • POST /recordsclient_token: true (unchanged from Round B; now citation-backed).
  • POST /records/batch_createclient_token: true (unchanged; citation-backed).
  • PUT /records/{record_id} → added per_method.PUT.client_token: true to the combined entry (citation-backed flip).
  • DELETE /records/{record_id}per_method.DELETE.client_token: false.
  • POST /records/batch_updateflipped from true to false (Round B carried the optimistic default; the official doc says it is not supported).
  • POST /records/batch_deletefalse (unchanged).
  • notes.oq8_citation block records the exact source-URL set + summary.

Test tests/test_configs_parse.py::test_lark_api_limits_yaml_parses_with_expected_keys was updated to assert the new per-method overrides and the citation note.


7. GPG backup backend

7.1 Implementation surface

lark_client.gpg_backup exposes:

  • GpgPublicKeyBackup(public_key_path=… | public_key_data=… | recipient_fpr=…, backup_dir=…, gnupg_home=…, gpg_binary="gpg") — constructor enforces "exactly one key source" via BackupError(stage="missing_key").
  • encrypt(ctx, payload) -> BackupResult(path, sidecar_path, key_fingerprint, ciphertext_sha256, bytes_ciphertext) — full result form.
  • __call__(ctx, payload) -> str — SafetyLayer-compatible (returns the .gpg path, the backup_ref).
  • BackupError(stage) — stages: missing_gpg, missing_key, key_import, encrypt, write, sidecar.
  • fail_closed_no_pubkey(reason) — helper to wire SafetyLayer fail-closed when secret is missing.

7.2 Operational invariants honored

  • VPS holds only the public key (never the private key).
  • VPS never decrypts (no --decrypt flow in the module).
  • Encryption uses an isolated GNUPGHOME (default $TMPDIR/lark-gpg-home-<pid>) so nothing leaks into the operator's keyring.
  • Plaintext payload written to a TemporaryDirectory, encrypted, then _secure_delete() (zero-fill + unlink) before the directory is cleaned up. Test test_plaintext_not_left_behind proves the plaintext path no longer exists after encrypt() returns.
  • Ciphertext output: 0o640, fsync'd before close, day-sharded under <backup_dir>/YYYYMMDD/.
  • Sidecar <out>.meta.json stores key fingerprint and sha256(ciphertext) — never the plaintext digest, never the raw payload, never any key material.
  • gpg binary missing → BackupError(stage="missing_gpg").
  • gpg --encrypt non-zero exit → BackupError(stage="encrypt").
  • gpg --import non-zero exit → BackupError(stage="key_import").

7.3 LARK_BACKUP_GPG_PUBKEY status

By reference only (gcloud secrets list --filter='name~LARK' --format='value(name)'):

LARK_APP_ID
LARK_APP_SECRET

LARK_BACKUP_GPG_PUBKEY is still absent as of 2026-05-20 11:35 UTC+7. No value was read.

Round C factory.build_service() resolves the GPG backend at runtime:

  1. LARK_BACKUP_GPG_PUBKEY_PATH env var → readable file → GpgPublicKeyBackup(public_key_path=…).
  2. LARK_BACKUP_GPG_PUBKEY_FPR env var → 40 hex chars → GpgPublicKeyBackup(recipient_fpr=…).
  3. Neither present → falls back to the Round B _default_gpg_backup stub (audit trail still records a stub marker).

Production backup execution remains blocked until the secret lands — but the wiring is shape-complete.

7.4 GPG test evidence

tests/test_gpg_backup.py — 10 tests:

  1. test_constructor_requires_exactly_one_key_source — refuses zero / multiple sources with BackupError(stage="missing_key").
  2. test_encrypt_mocked_subprocess — full encrypt path with a mocked subprocess.run, verifies .gpg + .meta.json written, sidecar shape correct.
  3. test_call_returns_string_path — SafetyLayer-compatible __call__ returns the ciphertext path.
  4. test_plaintext_not_left_behind — plaintext file path passed to mock-gpg no longer exists after encrypt() returns.
  5. test_missing_gpg_raises_BackupErrorshutil.which patched to return NoneBackupError(stage="missing_gpg").
  6. test_gpg_encrypt_failure_raises_BackupError — mocked encrypt fails → BackupError(stage="encrypt").
  7. test_parse_first_fpr — parses gpg --with-colons --list-keys output correctly.
  8. test_secure_delete — zero-fill + unlink works on a tmp file.
  9. test_normalize_fpr_strips_whitespace — accepts a fingerprint with spaces and normalises.
  10. test_real_gpg_roundtrip — generates a transient RSA-2048 key in an isolated GNUPGHOME, exports the public half, runs GpgPublicKeyBackup.encrypt(...) end-to-end, asserts the output is an OpenPGP packet and the sidecar fingerprint length is 40.

Result: 10 passed. Real GPG works on the VPS; no GSM provisioning was needed.


8. PII scanner

8.1 Implementation surface

lark_client.pii exposes:

  • PiiScanner(registry_path=… | registry=…, patterns=…).
  • PiiScanner.from_yaml(path) — convenience constructor.
  • scan(*, base_key, table_id, payload) -> dict — pure metadata-only.
  • __call__(ctx, payload, *, egress_blocked=False) -> dict — SafetyLayer-compatible. When egress_blocked=True AND any PII detected, raises SafetyViolation(reason="pii_egress_blocked").

8.2 Returned metadata (addendum §C2 shape)

{
  "pii_redacted": true,
  "redaction_types": ["national_id_cccd", "phone_vn"],
  "redacted_fields_count": 2,
  "detector": ["pattern", "registry"],
  "field_hits": ["Số CCCD", "Số điện thoại"]
}
  • pii_redacted — true if any pattern or registry entry matched.
  • redaction_types — sorted list of category labels from the pii_types enum.
  • redacted_fields_count — distinct field labels (not raw values).
  • detector — sorted subset of {"registry","pattern"} or ["none"].
  • field_hits — safe field labels only (no matched substring).

8.3 Vietnamese patterns shipped

Type Regex Notes
national_id_cccd (?<!\d)\d{12}(?!\d) CCCD 12 digits, isolated from other digits.
national_id_cmnd (?<!\d)\d{9}(?!\d) CMND 9 digits.
passport_vn \b[BCGN]\d{7}\b (case-insensitive) B/C/G/N prefix + 7 digits.
phone_vn (?<!\d)(?:\+?84|0)(?:3|5|7|8|9)\d{8}(?!\d) 10-digit mobile starting 03/05/07/08/09 (and +84 form).
bank_account (?<!\d)\d{10,19}(?!\d) Context-sensitive — kept for completeness; co-fires with CCCD on the 12-digit overlap.
email RFC-lite [A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}.
tax_code_vn 10 or 13 digits, optional - Low-risk.

8.4 PII test evidence

tests/test_pii.py — 18 tests:

  • Registry path: file load, hit by field_name, miss across base/table, wildcard field_name: "*".
  • Pattern path: parametrized cases for CCCD, CMND, phone 09/08, +84, passport, email.
  • Metadata-only invariant: the raw secret (CCCD or phone) cannot appear in any part of the result dict (repr(res) + str(res)).
  • Clean-payload path returns pii_redacted=False, detector=["none"].
  • Batch payload walked (records list).
  • Attachment nested dict walker doesn't crash.
  • Egress-blocked: raises SafetyViolation(reason="pii_egress_blocked") on detection; passes when clean.
  • Production registry parses: config/pii-fields.yaml loads and Base 88 / tblKnzaih6154r2e has ≥5 entries.

Result: 18 passed.

8.5 Sample CLI surface (Cowork-facing)

records update --dry-run with a payload containing a 12-digit CCCD-like value:

{
  "ok": true,
  "mode": "dry_run",
  "operation": "record.update",
  "base_key": "88-phai-cu-base-dem",
  "table_id": "tblPQ6N79EeOmnTm",
  "record_id": "recPII",
  "would_write": {"fields": {"Số CCCD": "012345678901"}},
  "diff": [],
  "approval_status": "exempt",
  "safety_checks_passed": ["preflight", "dry_run_or_confirm", "approval", "backup", "audit_planned", "lock", "pii_scan"],
  "warnings": [],
  "operation_id": "<uuid>",
  "audit_ref": "<uuid>",
  "pii": {
    "pii_redacted": true,
    "redaction_types": ["national_id_cccd", "address", "attachment_doc", "bank_account", "email", "national_id_cccd", "passport_vn", "phone_vn"],
    "redacted_fields_count": 1,
    "detector": ["pattern", "registry"],
    "field_hits": ["Số CCCD"]
  }
}

(The exact redaction_types list reflects the union of pattern detections + the Base đệm wildcard registry entry on this table.)


9. File-lock acquirer

9.1 Implementation surface

lark_client.locks exposes:

  • FileLockAcquirer(lock_dir=…, timeout_s=0.0, poll_interval_s=0.05) — SafetyLayer-compatible callable. Default lock_dir from LARK_LOCK_DIR env var or /var/lock/lark-ops.
  • FileLockAcquirer.__call__(ctx) returns a context manager (_FileLock).
  • FileLockAcquirer.acquire(ctx, *, timeout_s=...) for bounded blocking.
  • LockTarget.for_ctx(ctx) derives (base_key, table_id, record_id, granularity).
  • lock_path_for(target, root) computes deterministic on-disk path.
  • take_lock(ctx, *, lock_dir, timeout_s) convenience helper for direct use.

9.2 Granularity decision (addendum §C3)

op granularity path
record.update, record.delete (record_id present) record-level <root>/<base>/<table>/<record>.lock
record.create table-level <root>/<base>/<table>.lock
record.batch_* table-level <root>/<base>/<table>.lock
update/delete with empty record_id (fail-safe) table-level <root>/<base>/<table>.lock

Batch operations deliberately do not acquire many record-level locks (which would risk deadlock under N concurrent CLI invocations on overlapping batches).

9.3 Behaviour

  • fcntl.flock(LOCK_EX | LOCK_NB) on the fd of the lock file.
  • Contention → SafetyViolation(reason="lock_held") immediately (non-blocking default).
  • timeout_s > 0 → bounded poll loop (poll_interval_s default 0.05 s) until the lock acquires or the deadline passes.
  • Stale lockfiles on disk are intentional and not deleted — fcntl is advisory and the kernel releases on process death anyway; on-disk files are kept for forensic inspection.
  • All path segments sanitised: [A-Za-z0-9_\-\.]{1..128} only, any other char becomes _. Path-traversal segments (..) are reduced to underscores in surrounding context (test test_path_segments_sanitized asserts no .. or . directory component appears in any segment).

9.4 Lock test evidence

tests/test_locks.py — 16 tests:

  • Parametrized granularity table (7 cases).
  • Deterministic path layout for record-level and table-level.
  • Sanitization for b/../evil, t/x, rec X style inputs.
  • Acquire/release happy path.
  • Re-entry from same process after release.
  • Cross-process contention (multiprocessing.Process holds the lock, parent immediately raises SafetyViolation(reason="lock_held")).
  • Bounded blocking (peer releases after 0.2 s, parent acquires with 2 s timeout in well under 1.9 s).
  • LARK_LOCK_DIR env override.
  • take_lock(...) convenience context-manager helper.

Result: 16 passed.


10. WriteOutcome record-ID surface (addendum §C4)

WriteOutcome (in lark_client/safety.py) gained:

created_record_id: Optional[str] = None
created_record_ids: list[str] = field(default_factory=list)
record_id: Optional[str] = None
record_ids: list[str] = field(default_factory=list)

Populated by _populate_record_ids(outcome, ctx, response):

  • record.createcreated_record_id from response.data.record.record_id.
  • record.batch_createcreated_record_ids from response.data.records[*].record_id.
  • record.update/record.deleterecord_id from ctx (echoed even in dry-run) or from response.data.record.record_id.
  • record.batch_update/record.batch_deleterecord_ids derived from payload ({records: [{record_id:…}, …]} or {records: ["rec…", …]}) or from response.

lark_response_meta is whitelisted to {code, msg, http_status, request_id, server_time} — the raw record body is never echoed, preventing PII leakage through the audit/result path.

CLI emitters (cli/records.py) call _attach_record_id_surface(body, ctx, outcome) which surfaces the ids only when populated (keeps the dry-run preview clean):

{
  "ok": true,
  "mode": "live",
  "operation": "record.create",
  …,
  "created_record_id": "recNEW123",
  "pii": { "pii_redacted": false, …, "field_hits": [] }
}

11. End-user scenarios (addendum §C6)

tests/test_round_c_scenarios.py — 8 tests; all run with mocked api_caller. No live Lark API call.

Scenario Test Outcome
C6-1 records create on Base đệm with mocked success test_scenario_create_on_buffer_base_emits_created_record_id WriteOutcome.created_record_id == "recNEW123", approval_id=="EXEMPT", all 6 safety layers ran.
C6-2 records update with CCCD-like payload test_scenario_update_with_cccd_payload_detects_pii outcome.pii.redaction_types includes national_id_cccd; raw CCCD value not in pii blob.
C6-3 records delete with backup + approval consume test_scenario_delete_invokes_backup_and_consumes_approval GPG backup callable invoked exactly once; outcome.approval_id="APR-001"; YAML mark-used persisted.
C6-4 concurrent lock contention test_scenario_concurrent_lock_contention_one_wins Child holds lock → parent raises SafetyViolation(reason="lock_held").
C6-5 GPG missing failure path test_scenario_gpg_missing_blocks_destructive_write BackupError(stage="missing_gpg") raised; api_caller never invoked.
CLI dry-run create JSON shape test_cli_dry_run_create_emits_addendum_1_shape Structured JSON includes pii, safety_checks_passed, approval_status.
CLI live create surfaces id test_cli_live_create_emits_created_record_id body["created_record_id"]=="recCLI42".
CLI dry-run with CCCD test_cli_dry_run_carries_pii_metadata_for_cccd body["pii"].redaction_types includes national_id_cccd; raw value absent.

Result: 8 passed.


12. Tests run and results

$ ssh contabo 'cd /opt/incomex/lark-client && env -u LARK_TEST_INTEGRATION .venv/bin/pytest -q'
.....................................................................
.....................................................................
134 passed, 5 skipped in 4.93s
Test file Pass Skip Net new
tests/test_approval.py 17 0 — (Round B baseline)
tests/test_audit_extension.py n 0
tests/test_cli_records.py 8 0
tests/test_cli_smoke.py 4 0
tests/test_configs_parse.py 5 0 updated for Round C seed + OQ-8
tests/test_core.py n n
tests/test_core_write.py n 0
tests/test_gpg_backup.py 10 0 +10
tests/test_locks.py 16 0 +16
tests/test_pii.py 18 0 +18
tests/test_reader.py n 0
tests/test_registry.py n 0
tests/test_round_b1.py 12 0
tests/test_round_c_scenarios.py 8 0 +8
tests/test_safety.py 13 0

Net new Round C tests: +52 across 4 new test files + 1 updated.

The 5 skipped tests are the pre-existing integration tests guarded by LARK_TEST_INTEGRATION=1 (Base đệm live probe), unchanged from Round A/B.

12.1 Proof cold tests do not call live Lark API

  • tests/test_core_write.py lint test test_no_request_or_requests_outside_core still active and green; nothing outside core.py imports requests or calls _request.
  • All Round C tests either use unittest.mock.MagicMock for LarkCore / LarkReader / Registry, or mock subprocess.run (GPG path). No test instantiates a real LarkCore with credentials.
  • The Round B _round_b_refusing_api_caller is still the default api_caller wired by the factory; mutating CLI invocations land on it unless a test injects a mock.
  • Cold pytest command: env -u LARK_TEST_INTEGRATION .venv/bin/pytest -qLARK_TEST_INTEGRATION explicitly unset; the integration marker skips collection.

13. config/bases.yaml integrity + LARK_BACKUP_GPG_PUBKEY status

13.1 bases.yaml sha256

When SHA256
Before Round C (= Round B final) c063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d
After Round C c063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5d

Unchanged. No fixture, test, or scanner touched the production registry — they use tmp_path synthetic YAML.

13.2 LARK_BACKUP_GPG_PUBKEY presence (by reference)

$ gcloud secrets list --filter="name~LARK" --format="value(name)"
LARK_APP_ID
LARK_APP_SECRET

LARK_BACKUP_GPG_PUBKEY is still absent. No secret value was read.


14. Confirmations

  • No Lark write — every Round C test uses mocked api_caller; the default _round_b_refusing_api_caller still wired by the factory.
  • No production touched — production base 88-phai-cu (YSIkb8PxOaNaozs2vwalOOcagkf) never contacted.
  • No Base đệm write — Round C tests reach the API only via mocks; the buffer base is mentioned only in fixtures.
  • No deploy — no service restart, no Docker action, no nginx reload.
  • No push / merge / tagfeat/s177-sprint1-round-a remains local.
  • No MCP write enablementcli/records.py still defaults to the Round B refusing api_caller.
  • No new bot, no credential rotation, no secret printedgcloud secrets list --filter='name~LARK' --format='value(name)' was the only secret-touching call; no value read.
  • No KB docs created outside knowledge/dev/lark/s177-controlled-crud-gateway/.
  • No self-advance to Round D or Sprint 2 (MCP adapter remains gated).

15. Recommendation for Round D

The system is now write-ready in shape, with three remaining gates before any authorized live write:

  1. GSM provisioning of LARK_BACKUP_GPG_PUBKEY. Stage either the public-key file path under LARK_BACKUP_GPG_PUBKEY_PATH or pre-import the fingerprint and set LARK_BACKUP_GPG_PUBKEY_FPR. The factory wires GpgPublicKeyBackup automatically when one of these is present.
  2. Authorized live write integration on Base đệm. Suggested first slice: records create --confirm against tblPQ6N79EeOmnTm with a trivial fields payload, with the api_caller replaced from _round_b_refusing_api_caller to a real LarkCore.write. Cowork-facing JSON already surfaces created_record_id.
  3. Sprint 2 MCP adapter authorization, building on the same WriteContext.source = "mcp" plumbing tested in Round C scenario C6-1.

Optional polish for Round D:

  • Extend the PII registry beyond Base 88 once snapshot data for the remaining 17 bases lands (addendum §C7 explicitly defers this).
  • Refresh _populate_record_ids once we have a verified live response shape (the current parsing follows the official-doc structure but may need tightening on edge cases).
  • Add a records.approve CLI for runtime approval registration (currently approvals must be hand-edited into config/write-approvals.yaml).

16. STOP

Round C execution complete. Round C is the new baseline for any Round D / Sprint 2 authorization decision. No further self-advance.

Back to Knowledge Hub knowledge/dev/lark/s177-controlled-crud-gateway/s177-sprint1-round-c-implementation-evidence-2026-05-20.md