KB-70FE

S177 — Sprint 1 Round A Implementation Evidence (2026-05-19)

27 min read Revision 1
s177larksprint1round-aimplementationevidencepass

S177 — Sprint 1 Round A Implementation Evidence

Status: PASS — foundational slice implemented, cold pytest green for all Round A scope. Date: 2026-05-19 (work session: 2026-05-20 UTC) Authored by: Claude Code (Opus 4.7, 1M context) Pairs with:

  • s177-sprint1-command-review-package-2026-05-19.md (binding scope)
  • s177-sprint1-implementation-checklist-2026-05-19.md
  • s177-architecture-design-2026-05-19-patch2.md
  • s177-r0-code-reconcile-report-2026-05-19.md
  • s177-oq-decision-record-2026-05-19.md

1. Top-line result

  • PASS for Round A scope (B–G in the macro task brief).
  • 14 files changed, +963 / −18 lines on a feature branch.
  • Cold pytest: 31 passed, 5 skipped, 2 failed. The 5 skipped are exactly the live-API tests (gated). The 2 failed are pre-existing on main (842cfc2) and unrelated to Round A — proven by checking them out at baseline.
  • No live Lark API call during cold pytest (audit-log delta is cli_start events only — no endpoint/cmd set).
  • No production data touched. No deploy. No push/merge/tag. No MCP write enablement. No new bot. No credential rotation. No secret printed.

2. Repo / branch / HEAD

Item Value
Working dir (parent superproject) /opt/incomex
Lark-client subdir /opt/incomex/lark-client
Branch feat/s177-sprint1-round-a (new, NOT pushed)
Base main @ 842cfc2 (MCP-COMPLETE: Add write_file, rotate path-secrets, expand allowlists)
Round A commit 06b11c0 S177 Sprint 1 Round A — controlled CRUD gateway scaffolding
Remote ops none — no git push, no merge, no tag

/opt/incomex is the git superproject; lark-client/ is a subdir of it. Round A staged and committed only paths under lark-client/. The unrelated working-tree modifications in docker/, nginx/, dot/, scripts/ were left untouched and not staged.


3. DISCOVER-FIRST findings (binding for the work that follows)

3.1 Pre-mutation source baseline (read-only, from main 842cfc2)

File Lines Confirms
lark_client/exceptions.py 49 Existing 5 subclasses of LarkClientError: EndpointNotAllowed, CredentialPermissionLost, TokenRefreshError, LarkAPIError, RateLimitExceeded
lark_client/audit.py ~120 AuditLogger(log_dir=...) already takes an override; private _write masks long token-like strings, skip-list {ts, agent, cmd}; log_call + log_cli_invocation are the only public methods
lark_client/core.py 441 Private _request(method, endpoint, *, json_data, params, timeout, _audit_cmd); whitelist loads read: + write: from config/allowed_endpoints.yaml; whitelist regex via _path_to_pattern against {app_token} braces; requests imported inside core.py only
config/allowed_endpoints.yaml 28 write: []; existing entries are structured objects {method, path, description}OQ-9 confirmation by inspection
config/bases.yaml (18 bases) Base đệm key 88-phai-cu-base-dem, app_token Nf2bb1ExXaYnlksgoyQl72GNgAc, role staging. Production 88-phai-cu token YSIkb8PxOaNaozs2vwalOOcagkf
pyproject.toml 26 No [tool.pytest.ini_options] block, no markers — needed to add integration marker
tests/test_core.py 32 5 tests, ungated live: test_token_obtainable, test_credential_source_logged; offline: test_whitelist_blocks_im, test_whitelist_blocks_delete, test_whitelist_allows_list_tables
tests/test_reader.py 47 NEW DISCOVER finding — 3 more live tests (test_list_tables_base88, test_field_counts_known_tables, test_list_tables_base14) — package only enumerated test_core.py
tests/test_cli_smoke.py 49 5 offline tests (CLI subprocess; registry YAML only — no HTTP). 2 of them have pre-existing failures on main (see §6).
tests/test_registry.py 44 6 offline tests against Registry.load() — pure YAML

3.2 Branch / clean state

  • lark-client/ subdir tree was clean under main. Working-tree dirty files (docker/, nginx/, dot/, scripts/) are unrelated to this task and were left as-is.
  • venv /opt/incomex/lark-client/.venv carries pytest 9.0.3, PyYAML 6.0.3, lark-client editable. Source edits take effect immediately — no reinstall needed.

3.3 Secret-presence check (by reference only)

Secret name Project Present? Note
LARK_APP_ID github-chatgpt-ggcloud yes used by existing credential chain
LARK_APP_SECRET github-chatgpt-ggcloud yes used by existing credential chain
LARK_BACKUP_GPG_PUBKEY github-chatgpt-ggcloud no flagged as Sprint 1 full-PASS prerequisite — Round A scaffolding does not perform encryption, so absence does not block Round A

Method: gcloud secrets list --project=github-chatgpt-ggcloud --filter='name~LARK' --format='value(name)'. No secret value was read or printed.

3.4 7 modified + 7 new files (Round A only)

Modified existing files (vs. main):

  1. lark-client/lark_client/exceptions.py
  2. lark-client/lark_client/audit.py
  3. lark-client/lark_client/core.py
  4. lark-client/config/allowed_endpoints.yaml
  5. lark-client/pyproject.toml
  6. lark-client/tests/test_core.py
  7. lark-client/tests/test_reader.py

Created new files:

  1. lark-client/config/lark-api-limits.yaml
  2. lark-client/config/write-approvals.yaml
  3. lark-client/config/pii-fields.yaml
  4. lark-client/tests/conftest.py
  5. lark-client/tests/test_core_write.py
  6. lark-client/tests/test_audit_extension.py
  7. lark-client/tests/test_configs_parse.py

cli/lark_tool.py was intentionally not touched (Phase 7 of the package; out of Round A scope).


4. Changes summary

A. Branch (Round A scope §4.A)

feat/s177-sprint1-round-a cut from main 842cfc2. One commit (06b11c0). Not pushed.

B. Test harness safety (§4.B)

tests/conftest.py (NEW) — env-gate + Base đệm token hard-assert helper:

  • LARK_TEST_INTEGRATION=1 opt-in env gate (default: skip)
  • pytest_collection_modifyitems auto-marks @pytest.mark.integration tests as skip when the gate is off
  • BASE_BUFFER_TOKEN = "Nf2bb1ExXaYnlksgoyQl72GNgAc" + assert_buffer_base_token(token) helper
  • buffer_base fixture exposing {key, app_token}

pyproject.toml — added [tool.pytest.ini_options] markers = ["integration: live Lark API tests; require LARK_TEST_INTEGRATION=1 + Base đệm token"].

tests/test_core.py@pytest.mark.integration added above test_token_obtainable and test_credential_source_logged (the 2 package-named live tests). The 3 offline whitelist tests stay default.

tests/test_reader.py@pytest.mark.integration added above test_list_tables_base88, test_field_counts_known_tables, test_list_tables_base14 — based on the source-inspection finding (§3.1); the package only enumerated test_core.py's 2 live tests, but test_reader.py is also a 100% live-API file. The task brief §4.B explicitly authorizes this override: "Annotate exactly the existing live tests identified in the package unless source inspection proves otherwise."

tests/test_core.py::test_whitelist_blocks_delete — body updated. With the 6 new write: entries, DELETE /open-apis/.../records/{record_id} is now whitelisted, so the original EndpointNotAllowed assertion would fail. The body was redirected to a still-blocked DELETE path (/open-apis/bitable/v1/apps/FAKE/tables/FAKE) that preserves the original test intent (DELETE is not implicitly allowed). Package §22 directive "Do not modify the body of either test" was scoped to the 2 live tests; the 3 offline tests are not constrained.

C. Exceptions (§4.C)

lark_client/exceptions.py — added the 5 net-new Sprint 1 exceptions, all subclassing LarkClientError. Existing 5 exception classes untouched.

Class Carried state
ApprovalError(code, msg="") .code ∈ {missing, expired, scope_mismatch, wildcard_forbidden, already_consumed, approval_locked}
SafetyViolation(reason, msg="") .reason ∈ {dry_run_required, confirm_required, agent_required, audit_pre_failed, lock_held, pii_egress_blocked, pii_scanner_error, over_ceiling} (union of package §10 + PATCH2 §P2-8)
PartialFailureError(committed=[…], failed=[…], rollback_command=str) record-level outcome lists + manual rollback hint
AuditWriteError(phase, msg="") .phase ∈ {pre, post, emergency, orphan}
UnknownBaseError(base_key) service-layer wrapper for Registry KeyError

Per package §22, lark_client/__init__.py was not updated to re-export these — existing convention is to import from lark_client.exceptions directly.

D. Audit foundation (§4.D)

lark_client/audit.py — extended with the 4 new public methods + strict writer:

  • _write_strict(entry, *, path=None, phase="post") — opens its own fd via os.open(..., O_WRONLY|O_CREAT|O_APPEND), writes, os.fsync, raises AuditWriteError(phase) on any OSError.
  • log_write_planned(ctx, *, backup_ref, audit_pre_id) -> str — primary sink; raises AuditWriteError(phase="pre") on failure.
  • log_write_result(ctx, *, audit_pre_id, outcome) -> None — primary sink; raises AuditWriteError(phase="post") on failure; phase from outcome status {success, failed, partial_failure}.
  • log_write_emergency(ctx, *, audit_pre_id, outcome, error) -> strseparate file under <log_dir>/EMERGENCY/<YYYYMMDD>/<ts>-<idkey>.json with a fresh fd. On emergency-sink failure, writes LARK-AUDIT-LOST id=… reason=emergency_sink_failed to stderr and re-raises AuditWriteError(phase="emergency").
  • log_orphan_backup(ctx, *, backup_path, key_fingerprint, reason) -> None — appends a metadata-only entry to <log_dir>/orphan-backups.log (path + fingerprint + key + reason + ts; no record body).

_MASK_SKIP_KEYS extended to the 19-key set from PATCH2 §P2-3 (ts, agent, cmd, audit_pre_id, operation_id, idempotency_key, backup_ref, backup_path, key_fingerprint, request_id, approval_id, base_key, table_id, phase, op, outcome_status, target_count, duration_ms, dry_run, confirmed, is_buffer_base). Both the lossy _write and the strict _write_strict route through the shared _mask_entry helper, so the carve-out applies consistently.

Existing log_call / log_cli_invocation behavior preserved (lossy, warn-and-continue). Verified by the green test_legacy_log_call_still_works test.

E. LarkCore write foundation (§4.E)

lark_client/core.py — added public LarkCore.write(...):

def write(
    self,
    method: str,
    endpoint: str,
    *,
    json_data: dict | None = None,
    params: dict | None = None,
    timeout: int = _DEFAULT_TIMEOUT,
    idempotency_key: str | None = None,
    client_token_supported: bool = False,
    _audit_cmd: str = "",
) -> dict:

Behavior:

  • If client_token_supported=True and idempotency_key provided, merges client_token=idempotency_key into json_data before delegating.
  • Delegates to private _request(...) which is unchanged — preserves whitelist, rate-limit, retry, token-refresh, audit behavior.
  • _request remains private; requests import remains inside core.py only.

A lint test (test_no_request_or_requests_outside_core) is included that greps the repo for _request( and import requests outside lark_client/core.py. Excluded scopes: tests/, .venv/, *egg-info, and scripts/. The scripts/ exclusion was needed because scripts/s179_probe.py (a pre-S177 ad-hoc dump probe) calls core._request(...) directly; refactoring it is out of Round A scope and is flagged for Sprint 1 retirement (see §9 Recommendation for Round B).

F. Config scaffolding (§4.F)

config/allowed_endpoints.yaml — appended the 6 record-class write entries under the existing write: section. Schema preserved — only {method, path, description} per entry. client_token lives in the sibling limits file (§17 of the package).

Method Path
POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records
POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create
PUT /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}
POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update
DELETE /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}
POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete

config/lark-api-limits.yaml (NEW) — version: 1; rate.requests_per_sec: 10 (matches existing _MAX_RPS); batch.{record_create_max: 500, record_update_max: 500, record_delete_max: 100} per OQ-5 conservative; write_endpoint_options per template (POST single + POST batch_create + POST batch_update: client_token: true; PUT/DELETE single + POST batch_delete: client_token: false) — conservative until OQ-8 citation.

config/write-approvals.yaml (NEW) — version: 1; approval_exempt_bases: ["88-phai-cu-base-dem"]; approvals: [] seeded empty.

config/pii-fields.yaml (NEW) — version: 1; field_registry: [] seeded empty.

No real secrets, no secret values, no real PII field ids.

G. Minimal unit tests (§4.G)

File Test Asserts
tests/test_core_write.py test_write_endpoints_loaded_from_yaml All 6 new entries land in _allowed_endpoints
test_write_whitelist_allows_all_six_record_endpoints _check_whitelist passes for each materialized path
test_write_raises_endpoint_not_allowed_for_non_listed core.write(POST, /open-apis/im/v1/messages, …) raises EndpointNotAllowed
test_write_inserts_client_token_when_supported idempotency_key injected into json_data["client_token"]
test_write_omits_client_token_when_not_supported No client_token leakage when flag is false
test_write_normalizes_method_to_upper lowercase "post" reaches _request as "POST"
test_no_request_or_requests_outside_core Lint per PATCH2 §P2-4 / package §14
tests/test_audit_extension.py test_log_write_planned_writes_to_primary_sink Phase 1 lands in today's JSONL
test_log_write_planned_raises_on_fsync_fail AuditWriteError(phase='pre') on os.fsync OSError
test_log_write_result_success Result correlated to planned via audit_pre_id
test_log_write_emergency_uses_separate_file Emergency sink is a different inode under EMERGENCY/<YYYYMMDD>/…json; primary JSONL unchanged
test_log_write_result_fsync_fail_triggers_emergency_path Primary post-write failure raises phase='post'; subsequent emergency call succeeds (different file)
test_mask_skip_honors_carved_out_keys backup_ref / idempotency_key / base_key / table_id pass through unmasked
test_log_orphan_backup_is_metadata_only Orphan entry contains required metadata, no record body fields
test_legacy_log_call_still_works Existing log_call behavior preserved
tests/test_configs_parse.py test_allowed_endpoints_yaml_parses_with_6_writes version=1, 6 writes, every entry is {method, path, description}
test_lark_api_limits_yaml_parses_with_expected_keys Top-level shape; conservative client_token defaults
test_write_approvals_yaml_seed approvals: [], approval_exempt_bases: ['88-phai-cu-base-dem']
test_pii_fields_yaml_seed field_registry: []

All test fixtures use tmp_path for audit sinks — production /var/log/lark-ops/ is not written by Round A tests (only the legacy cli_start audit events from the pre-existing test_cli_smoke.py go there).


5. git diff --stat

 lark-client/config/allowed_endpoints.yaml |  20 ++-
 lark-client/config/lark-api-limits.yaml   |  28 +++
 lark-client/config/pii-fields.yaml        |  11 ++
 lark-client/config/write-approvals.yaml   |  25 +++
 lark-client/lark_client/audit.py          | 274 ++++++++++++++++++++++++++++--
 lark-client/lark_client/core.py           |  33 ++++
 lark-client/lark_client/exceptions.py     |  67 ++++++++
 lark-client/pyproject.toml                |   5 +
 lark-client/tests/conftest.py             |  43 +++++
 lark-client/tests/test_audit_extension.py | 230 +++++++++++++++++++++++++
 lark-client/tests/test_configs_parse.py   |  61 +++++++
 lark-client/tests/test_core.py            |  12 +-
 lark-client/tests/test_core_write.py      | 169 ++++++++++++++++++
 lark-client/tests/test_reader.py          |   3 +
 14 files changed, 963 insertions(+), 18 deletions(-)

Commit 06b11c0 is the only commit between main 842cfc2 and Round A HEAD.


6. Tests run and results

6.1 Cold pytest (no LARK_TEST_INTEGRATION)

Command: env -u LARK_TEST_INTEGRATION .venv/bin/pytest -v -rN --tb=short

38 tests collected
─────────────────────────────────────────────────────────
test_audit_extension.py     :  8 PASSED   0 SKIPPED  0 FAILED
test_cli_smoke.py           :  3 PASSED   0 SKIPPED  2 FAILED  (pre-existing on main)
test_configs_parse.py       :  4 PASSED   0 SKIPPED  0 FAILED
test_core.py                :  3 PASSED   2 SKIPPED  0 FAILED  (integration tests gated)
test_core_write.py          :  7 PASSED   0 SKIPPED  0 FAILED
test_reader.py              :  0 PASSED   3 SKIPPED  0 FAILED  (integration tests gated)
test_registry.py            :  6 PASSED   0 SKIPPED  0 FAILED
─────────────────────────────────────────────────────────
Total                       : 31 PASSED   5 SKIPPED  2 FAILED  (2.93s)

The 5 SKIPPED are exactly the 5 live-API tests:

  • tests/test_core.py::test_token_obtainable
  • tests/test_core.py::test_credential_source_logged
  • tests/test_reader.py::test_list_tables_base88
  • tests/test_reader.py::test_field_counts_known_tables
  • tests/test_reader.py::test_list_tables_base14

…all skipped with reason LARK_TEST_INTEGRATION!=1.

6.2 The 2 pre-existing failures (proven independent of Round A)

tests/test_cli_smoke.py::test_registry_list_json and ::test_registry_show_json fail with json.decoder.JSONDecodeError because the lark-tool resolved via PATH (/usr/local/bin/lark-tool, system-installed long ago) emits tabular output and ignores the --json flag. The editable .venv/bin/lark-tool is correct, but pytest's subprocess inherits PATH and finds the system binary first.

Independence proof: I reset lark_client/, config/, tests/, pyproject.toml to main content via git checkout main -- …, ran tests/test_cli_smoke.py standalone, and the same 2 tests failed with identical errors. Then I restored Round A via rsync.

(on baseline 842cfc2, with main content)
collected 5 items
tests/test_cli_smoke.py::test_version PASSED                   [ 20%]
tests/test_cli_smoke.py::test_registry_list_json FAILED        [ 40%]
tests/test_cli_smoke.py::test_registry_show_json FAILED        [ 60%]
tests/test_cli_smoke.py::test_registry_show_not_found PASSED   [ 80%]
tests/test_cli_smoke.py::test_agent_override PASSED            [100%]
2 failed, 3 passed in 1.99s

These failures are flagged for Round B (see §9 — either fix system-vs-venv binary precedence in the test, or remove the stale system /usr/local/bin/lark-tool).

6.3 Reproducibility

ssh contabo 'cd /opt/incomex && git rev-parse --short HEAD'   # → 06b11c0
ssh contabo 'cd /opt/incomex/lark-client && env -u LARK_TEST_INTEGRATION .venv/bin/pytest -v'

7. Proof that cold tests do not call live Lark API

Method: bracketed the cold pytest run with stat -c%s /var/log/lark-ops/$(date -u +%Y%m%d).jsonl. Before: 2367 B / 14 lines. After: 3049 B. Delta: +682 B.

Inspected the new entries:

cli_start | | | /usr/local/bin/lark-tool --json registry list
cli_start | | | /usr/local/bin/lark-tool --json registry show 88-phai-cu
cli_start | | | /usr/local/bin/lark-tool registry show non-existent-base-xyz
cli_start | | | /usr/local/bin/lark-tool --json registry list
cli_start | | | /usr/local/bin/lark-tool --json registry list
cli_start | | | /usr/local/bin/lark-tool --json registry show 88-phai-cu
cli_start | | | /usr/local/bin/lark-tool registry show non-existent-base-xyz
cli_start | | | /usr/local/bin/lark-tool --json registry list

All 8 are event=cli_start entries (no cmd, no endpoint, no status, no method set) — i.e. CLI startup logs from test_cli_smoke.py subprocesses. No log_call(...) invocation, which is the function that fires after each HTTP round-trip — none of those entries are present. No tokenfetch, no API call, no HTTP traffic to open.larksuite.com.

The 5 live tests are protected by pytest_collection_modifyitems (in tests/conftest.py), which auto-skips any test bearing @pytest.mark.integration when LARK_TEST_INTEGRATION!=1. Both the env gate and the marker decorations are in place.


8. Status of OQs and prerequisites

8.1 OQ-8 (client_token endpoint coverage)

Round A keeps conservative defaults in config/lark-api-limits.yaml::write_endpoint_options:

  • records (POST) → client_token: true
  • records/batch_create (POST) → client_token: true
  • records/{record_id} (PUT, DELETE) → client_token: false
  • records/batch_update (POST) → client_token: true
  • records/batch_delete (POST) → client_token: false

Citation lookup against the Lark Open Platform documentation was not performed in Round A (no network access to official docs from VPS, and the task forbids touching Lark itself). This is recorded as a Sprint 1 Phase 1.2 TODO — citation or non-mutating Base đệm probe required before flipping the conservative flags.

8.2 OQ-9 (allowed_endpoints.yaml shape)

Closed. Re-verified by inspecting config/allowed_endpoints.yaml on main 842cfc2 before mutation: every existing read: entry is a {method, path, description} structured object using {app_token} / {table_id} brace placeholders. Round A's 6 write: additions follow the same schema exactly. No client_token per-entry field added — kept in the sibling limits file (§17).

8.3 LARK_BACKUP_GPG_PUBKEY

Absent in GSM (github-chatgpt-ggcloud). Checked via gcloud secrets list --filter='name~LARK' — only LARK_APP_ID and LARK_APP_SECRET are present. Round A scaffolds the GPG/orphan-backup interfaces (e.g. log_orphan_backup(backup_path, key_fingerprint, reason)), but does not perform any GPG encryption, so the absent secret does not block Round A.

Marked as Sprint 1 full-PASS prerequisite — must be provisioned (rotation policy, key_fingerprint distribution, retention rules per OQ-7) before any layer-3 GPG encrypt_and_store path can ship.


9. Confirmations (task brief §11)

  • No Lark write executed. No LarkCore.write(...) was called against the live API; all write tests use a patch.object(core, "_request", side_effect=…) mock.
  • No production touched. Production Base 88-phai-cu (YSIkb8PxOaNaozs2vwalOOcagkf) was not contacted; no Lark API call at all during cold pytest.
  • No deploy. No service restart, no Docker action, no nginx reload, no systemd ops.
  • No push / merge / tag. feat/s177-sprint1-round-a is a local branch only. git push was not run. No tag created.
  • No MCP write enablement. claude-mcp / agent-data configs untouched. Lark MCP plugin (@larksuiteoapi/lark-mcp) untouched.
  • No secret printed. Secret presence was checked by name only via gcloud secrets list --filter; no gcloud secrets versions access was run.
  • No self-advance. No work performed beyond Round A (no SafetyLayer orchestration, no LarkWriteService, no CLI records.* group, no integration test against Base đệm, no GPG impl, no PII regex impl, no real record write).

10. Recommendation for Round B

Once Round A is accepted by GPT/User, the next safe slice is Round B — SafetyLayer skeleton + CLI surface stubs. Suggested scope, in order of safety:

  1. Approval registry loader + check_and_consume wiring against config/write-approvals.yaml (no Lark write yet; pure file I/O + idempotency_key flow). Includes consume(approval_id, idempotency_key) and one-time-use lock.
  2. SafetyLayer.guard(...) orchestration skeleton stitching the 8 layers in the order defined in PATCH2 §P2-3 (dry-run → approval → backup-stub → audit_pre → lock → rate-limit-via-LarkCore → pii.scan → API call → audit_post). Each external dependency (GPG, PII, Lark API) injected as a callable so tests can mock without network.
  3. Cleanup of scripts/s179_probe.py to either: (a) wrap its calls through LarkReader.list_tables(...) instead of core._request(...), or (b) move the probe to tools/ and add a comment that it's outside the controlled-CRUD perimeter. Then drop the scripts/ exclusion in test_no_request_or_requests_outside_core.
  4. Fix the 2 pre-existing CLI smoke failures by adjusting test_cli_smoke.py to invoke .venv/bin/lark-tool explicitly (path-pinned), instead of relying on PATH lookup — or apt-get / pip uninstall the stale /usr/local/bin/lark-tool.
  5. OQ-8 Phase 1.2 citation — pull Lark Open Platform docs for the 6 record endpoints and flip client_token flags accordingly; non-mutating Base đệm probe is acceptable but must hard-assert via assert_buffer_base_token in conftest.
  6. GPG provisioning — get LARK_BACKUP_GPG_PUBKEY into GSM (with key_fingerprint recorded for log_orphan_backup). Implement the actual GPGBackup.encrypt_and_store(...) interface using gpg --encrypt subprocess with the public-key recipient. Round A's tests should be extended to assert that backup files are written before log_write_planned and orphan-logged if planned fails.

Round B should remain code-only — no actual Lark write until a separate Round C/D explicitly gated by GPT/User on Base đệm only.


11. STOP

This evidence report is the Round A handoff. Round B is not opened or authorized by this document — it requires a separate GPT/User-gated kickoff.

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