S177 — Sprint 1 Round C Implementation Evidence (2026-05-20)
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.mds177-sprint1-round-a-implementation-evidence-2026-05-19.mds177-sprint1-command-review-package-2026-05-19.mds177-architecture-design-2026-05-19-patch2.mds177-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,
WriteOutcomecarries 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.yamlupdated 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)
- Base commit:
- 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.yamlSHA256 unchanged —c063dc00c6b71a56d06435baba3019fcdd33d92999c770f8bc6d784866bdae5dbefore 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) | 1cf7901 — S177 Sprint 1 Round B4 — LarkWriteService + CLI records surface |
| New commit (Round C) | 2307265 — S177 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 statusshowed clean feature-branch state except__pycache__byproducts; no surprise modifications.- HEAD at
1cf7901matched the Round B evidence. - All Round B files present and unmodified pre-C:
lark_client/{audit,approval,safety,service,core,reader,registry,exceptions}.pycli/{lark_tool,records}.pyconfig/{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.pydid not exist → safe to create.gpgbinary present on VPS (/usr/bin/gpg, GnuPG 2.4.4, libgcrypt 1.10.3)./var/lock/lark-ops/did not exist → tests usetmp_path; production wiring creates the directory lazily under theFileLockAcquirer.gcloud secrets list --filter='name~LARK'(by name only — no values read):LARK_APP_ID,LARK_APP_SECRET.LARK_BACKUP_GPG_PUBKEYstill absent.- Round B test suite re-run cold:
82 passed, 5 skippedbefore 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 /records→client_token: true(unchanged from Round B; now citation-backed).POST /records/batch_create→client_token: true(unchanged; citation-backed).PUT /records/{record_id}→ addedper_method.PUT.client_token: trueto the combined entry (citation-backed flip).DELETE /records/{record_id}→per_method.DELETE.client_token: false.POST /records/batch_update→ flipped fromtruetofalse(Round B carried the optimistic default; the official doc says it is not supported).POST /records/batch_delete→false(unchanged).notes.oq8_citationblock 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" viaBackupError(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.gpgpath, thebackup_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
--decryptflow 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. Testtest_plaintext_not_left_behindproves the plaintext path no longer exists afterencrypt()returns. - Ciphertext output:
0o640, fsync'd before close, day-sharded under<backup_dir>/YYYYMMDD/. - Sidecar
<out>.meta.jsonstores key fingerprint and sha256(ciphertext) — never the plaintext digest, never the raw payload, never any key material. gpgbinary missing →BackupError(stage="missing_gpg").gpg --encryptnon-zero exit →BackupError(stage="encrypt").gpg --importnon-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:
LARK_BACKUP_GPG_PUBKEY_PATHenv var → readable file →GpgPublicKeyBackup(public_key_path=…).LARK_BACKUP_GPG_PUBKEY_FPRenv var → 40 hex chars →GpgPublicKeyBackup(recipient_fpr=…).- Neither present → falls back to the Round B
_default_gpg_backupstub (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:
test_constructor_requires_exactly_one_key_source— refuses zero / multiple sources withBackupError(stage="missing_key").test_encrypt_mocked_subprocess— full encrypt path with a mockedsubprocess.run, verifies.gpg+.meta.jsonwritten, sidecar shape correct.test_call_returns_string_path— SafetyLayer-compatible__call__returns the ciphertext path.test_plaintext_not_left_behind— plaintext file path passed to mock-gpgno longer exists afterencrypt()returns.test_missing_gpg_raises_BackupError—shutil.whichpatched to returnNone→BackupError(stage="missing_gpg").test_gpg_encrypt_failure_raises_BackupError— mocked encrypt fails →BackupError(stage="encrypt").test_parse_first_fpr— parsesgpg --with-colons --list-keysoutput correctly.test_secure_delete— zero-fill + unlink works on a tmp file.test_normalize_fpr_strips_whitespace— accepts a fingerprint with spaces and normalises.test_real_gpg_roundtrip— generates a transient RSA-2048 key in an isolated GNUPGHOME, exports the public half, runsGpgPublicKeyBackup.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. Whenegress_blocked=TrueAND any PII detected, raisesSafetyViolation(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.yamlloads andBase 88 / tblKnzaih6154r2ehas ≥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. Defaultlock_dirfromLARK_LOCK_DIRenv 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_sdefault 0.05 s) until the lock acquires or the deadline passes.- Stale lockfiles on disk are intentional and not deleted —
fcntlis 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 (testtest_path_segments_sanitizedasserts 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 Xstyle inputs. - Acquire/release happy path.
- Re-entry from same process after release.
- Cross-process contention (
multiprocessing.Processholds the lock, parent immediately raisesSafetyViolation(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_DIRenv 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.create→created_record_idfromresponse.data.record.record_id.record.batch_create→created_record_idsfromresponse.data.records[*].record_id.record.update/record.delete→record_idfrom ctx (echoed even in dry-run) or fromresponse.data.record.record_id.record.batch_update/record.batch_delete→record_idsderived 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.pylint testtest_no_request_or_requests_outside_corestill active and green; nothing outsidecore.pyimportsrequestsor calls_request.- All Round C tests either use
unittest.mock.MagicMockforLarkCore/LarkReader/Registry, or mocksubprocess.run(GPG path). No test instantiates a realLarkCorewith credentials. - The Round B
_round_b_refusing_api_calleris 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 -q—LARK_TEST_INTEGRATIONexplicitly 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_callerstill 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 / tag —
feat/s177-sprint1-round-aremains local. - No MCP write enablement —
cli/records.pystill defaults to the Round B refusing api_caller. - No new bot, no credential rotation, no secret printed —
gcloud 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:
- GSM provisioning of
LARK_BACKUP_GPG_PUBKEY. Stage either the public-key file path underLARK_BACKUP_GPG_PUBKEY_PATHor pre-import the fingerprint and setLARK_BACKUP_GPG_PUBKEY_FPR. The factory wiresGpgPublicKeyBackupautomatically when one of these is present. - Authorized live write integration on Base đệm. Suggested first slice:
records create --confirmagainsttblPQ6N79EeOmnTmwith a trivial fields payload, with the api_caller replaced from_round_b_refusing_api_callerto a realLarkCore.write. Cowork-facing JSON already surfacescreated_record_id. - 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_idsonce 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.approveCLI for runtime approval registration (currently approvals must be hand-edited intoconfig/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.