commit 18e7ebf97d7549bb8c88fc683e1b6e36576be347 from: Isaac Meerleo date: Fri Jul 17 23:53:15 2026 UTC Preserve authoritative System epoch variants commit - 9169cf965b11b6344d26b8e48154b7d9c83feb83 commit + 18e7ebf97d7549bb8c88fc683e1b6e36576be347 blob - 6b49757fb26a23f949a702605c73ace91be74886 blob + bf114b2bd34b02e78443d612ca772aff624c3fbe --- docs/adr/0008-immutable-system-epochs.md +++ docs/adr/0008-immutable-system-epochs.md @@ -11,6 +11,14 @@ current `project_context` policy selects a stored vari other, while current Provider, Tool, credential, and authority configuration remains live. +The complete variants are authoritative opaque bytes, not checksums of the +stored components. Replay and in-memory copies import them verbatim after +version, length, field, and raw-summary-tail validation that does not invoke +composition code. Components remain useful for audit and Subagent +construction, but current composition code must never normalize a stored Lead +variant. A Compaction replaces only the raw summary tail while preserving each +exact stored variant prefix, including its historical wrapper. + `/clear` adopts the invocation's current base, reuses the Session-cached personal snapshot without re-reading it, and admits a new in-tree project snapshot only when `project_context yes`. Under `project_context no`, the new @@ -34,6 +42,9 @@ parsing model-written continuation notes. model-influenced file edits without the Owner's explicit `/clear` boundary. - Rebuilding stored variants on resume: this reintroduces the configuration and binary drift the epoch exists to remove. +- Treating stored components as an integrity hash for the complete variants: + that would make a valid old epoch depend on today's composition code and + fail closed on intended implementation evolution rather than corruption. - Stable Journal offsets for a retained suffix: storage layout, legacy records, and partial tails must not become semantic identifiers. blob - a99ea2eb75e7a4bf509218bbe4179107f3db3f1e blob + 17b42cb2275bf32869e5c535567f0ab05bef670a --- docs/compaction.md +++ docs/compaction.md @@ -19,6 +19,11 @@ file sets, and the exact Retained Projection. On resum recomposing either. Current Provider, model, Tool, credential, protection, write, and network policy remains live. +The source components are audit and Subagent inputs, not reconstruction +checksums. If composition code has changed since an epoch was written, +Compaction replaces only the exact old raw-summary tail and preserves the +entire stored prefix, including its historical summary wrapper. + External context changes are admitted only at a fresh Session or `/clear`. `/clear` adopts the invocation's current base, reuses the Session-cached personal snapshot without re-reading it, and re-reads the in-tree project file blob - 5364cf067c69941889e8ca1f030a52d1179585d6 blob + cffeea1d6d36fc90de905cd182af3d127326fa3d --- handoff/verification.md +++ handoff/verification.md @@ -133,9 +133,10 @@ states the property, not just examples: authoritative full/restricted variant, reason, cumulative structured file sets, and retained Projection block. Fresh start, clear, successful Compaction, and legacy migration synchronize before request/publication. - Unsupported, - missing, duplicate, inconsistent-derived, over-bound, and malformed fields - hard-fail; a partial final epoch is ignored and retried. Cross-process + Unsupported, missing, duplicate, over-bound, malformed-encoding, and + forbidden-NUL component fields hard-fail; independently authoritative + complete variants are never compared with a current-code recomposition. A + partial final epoch is ignored and retried. Cross-process fixtures change base configuration, context files, composition code, Provider/model, Tools, and authority policy, then prove current `project_context` selects exact stored bytes for all Provider codecs and blob - e361bf736cd133685ed8b034cdd4a8229e31a182 blob + 796a50f5e5ae3b70a0e1b0f3ca2f705bc6150b42 --- regress/journal/journal_test.c +++ regress/journal/journal_test.c @@ -203,6 +203,14 @@ test_system_epoch_roundtrip(void) static const u_char personal[] = { 'p', 'e', 'r', 0xfe }; static const u_char project[] = { 'p', 'r', 'o', 'j', 0xfd }; static const u_char summary[] = { 's', 'u', 'm', '\0', 0xfc }; + static const u_char authoritative_full[] = { + 'o', 'l', 'd', '-', 'f', 'u', 'l', 'l', '-', 'w', 'r', 'a', 'p', + 's', 'u', 'm', '\0', 0xfc + }; + static const u_char authoritative_restricted[] = { + 'o', 'l', 'd', '-', 'r', 'e', 's', 't', 'r', 'i', 'c', 't', + 'e', 'd', '-', 'w', 'r', 'a', 'p', 's', 'u', 'm', '\0', 0xfc + }; struct system_epoch_input input; struct system_epoch epoch; struct compaction_files *files; @@ -222,6 +230,15 @@ test_system_epoch_roundtrip(void) input.summary.len = sizeof(summary); system_epoch_init(&epoch); CHECK(system_epoch_build(&epoch, &input) == 0); + /* Simulate a stored epoch written by an older composition implementation: + * components remain auditable, while these complete Provider inputs are + * independently authoritative. */ + buf_reset(&epoch.full); + buf_add(&epoch.full, authoritative_full, + sizeof(authoritative_full)); + buf_reset(&epoch.restricted); + buf_add(&epoch.restricted, authoritative_restricted, + sizeof(authoritative_restricted)); files = compaction_files_new(); CHECK(compaction_files_add(files, COMPACTION_FILE_READ, "README.md", 9) == 0); @@ -1072,10 +1089,16 @@ test_epoch_malformed_and_bounds(void) make_epoch(&clear, SYSTEM_EPOCH_CLEAR, NULL, 0); make_epoch(&bad_clear, SYSTEM_EPOCH_CLEAR, "not empty", 9); - /* Stored complete variants must match the rebuilt components exactly. */ + /* A well-formed complete variant is authoritative even when it differs + * from today's component composition. */ epoch_session(&start, id, path); CHECK(path_change_after(path, "\"full\":\"", '0') == 0); - restore_fails(id); + journal_state_init(&state); + CHECK(journal_restore(dir, id, &state) == 0); + CHECK(state.have_epoch); + CHECK(buf_eq(&state.epoch.base, "pinned base", 11)); + CHECK(state.epoch.full.len > 0 && state.epoch.full.data[0] == '\0'); + journal_state_free(&state); epoch_session(&start, id, path); CHECK(path_change_after(path, "\"version\":", '9') == 0); blob - 03f2f68bd17979f0b2124a0f8c79a36b18a06687 blob + dfbc965abf7c9e7d4c5af994e970ec45e31809a6 --- regress/system_epoch/system_epoch_test.c +++ regress/system_epoch/system_epoch_test.c @@ -244,7 +244,7 @@ test_all_reasons_and_binary_summary(void) } static void -test_copy_failure_is_transactional(void) +test_copy_is_exact_and_transactional(void) { struct system_epoch_input input; struct system_epoch dst, src; @@ -261,17 +261,129 @@ test_copy_failure_is_transactional(void) input.base.len = 6; CHECK(system_epoch_build(&src, &input) == 0); src.full.data[0] = 'X'; - CHECK(system_epoch_copy(&dst, &src) == -1); - CHECK(dst.reason == SYSTEM_EPOCH_START); - CHECK(buf_is(&dst.base, "DESTINATION", 11)); - CHECK(buf_is(&dst.full, "DESTINATION", 11)); + CHECK(system_epoch_copy(&dst, &src) == 0); + CHECK(dst.reason == SYSTEM_EPOCH_COMPACT); + CHECK(buf_is(&dst.base, "SOURCE", 6)); + CHECK(buf_is(&dst.full, "XOURCE", 6)); src.version = SYSTEM_EPOCH_VERSION + 1; CHECK(system_epoch_copy(&dst, &src) == -1); - CHECK(buf_is(&dst.full, "DESTINATION", 11)); + CHECK(buf_is(&dst.full, "XOURCE", 6)); system_epoch_free(&src); system_epoch_free(&dst); } +static void +test_exact_import_and_summary_replacement(void) +{ + static const char full_prefix[] = "OLD-COMPOSER-FULL"; + static const char restricted_prefix[] = "OLD-COMPOSER-RESTRICTED"; + static const char marker[] = + "\n\nSummary of the conversation so far " + "(your own continuation notes):\n\n"; + static const u_char binary_summary[] = { 'N', '\0', 'W' }; + struct system_epoch_exact_input exact; + struct system_epoch imported, copy, compacted, repeated, keep, broken; + struct system_epoch_input input; + struct system_epoch_span summary; + size_t off; + + system_epoch_init(&imported); + system_epoch_init(©); + system_epoch_init(&compacted); + system_epoch_init(&repeated); + system_epoch_init(&keep); + system_epoch_init(&broken); + memset(&exact, 0, sizeof(exact)); + exact.version = SYSTEM_EPOCH_VERSION; + exact.reason = SYSTEM_EPOCH_START; + exact.base.data = "CURRENT-COMPONENT-BASE"; + exact.base.len = 22; + exact.personal.data = "PERSONAL"; + exact.personal.len = 8; + exact.project.data = "PROJECT"; + exact.project.len = 7; + exact.full.data = full_prefix; + exact.full.len = sizeof(full_prefix) - 1; + exact.restricted.data = restricted_prefix; + exact.restricted.len = sizeof(restricted_prefix) - 1; + CHECK(system_epoch_import(&imported, &exact) == 0); + CHECK(buf_is(&imported.base, "CURRENT-COMPONENT-BASE", 22)); + CHECK(buf_is(&imported.full, full_prefix, sizeof(full_prefix) - 1)); + CHECK(buf_is(&imported.restricted, restricted_prefix, + sizeof(restricted_prefix) - 1)); + CHECK(system_epoch_copy(©, &imported) == 0); + imported.full.data[0] = 'X'; + CHECK(buf_is(©.full, full_prefix, sizeof(full_prefix) - 1)); + + summary.data = binary_summary; + summary.len = sizeof(binary_summary); + CHECK(system_epoch_replace_summary(&compacted, ©, &summary) == 0); + CHECK(compacted.reason == SYSTEM_EPOCH_COMPACT); + CHECK(buf_is(&compacted.base, "CURRENT-COMPONENT-BASE", 22)); + CHECK(buf_is(&compacted.personal, "PERSONAL", 8)); + CHECK(buf_is(&compacted.project, "PROJECT", 7)); + CHECK(buf_is(&compacted.summary, binary_summary, + sizeof(binary_summary))); + off = sizeof(full_prefix) - 1; + CHECK(compacted.full.len == off + sizeof(marker) - 1 + + sizeof(binary_summary)); + CHECK(memcmp(compacted.full.data, full_prefix, off) == 0); + CHECK(memcmp(compacted.full.data + off, marker, + sizeof(marker) - 1) == 0); + CHECK(memcmp(compacted.full.data + compacted.full.len - + sizeof(binary_summary), binary_summary, + sizeof(binary_summary)) == 0); + off = sizeof(restricted_prefix) - 1; + CHECK(compacted.restricted.len == off + sizeof(marker) - 1 + + sizeof(binary_summary)); + CHECK(memcmp(compacted.restricted.data, restricted_prefix, off) == 0); + + summary.data = "NEXT"; + summary.len = 4; + CHECK(system_epoch_replace_summary(&repeated, &compacted, + &summary) == 0); + CHECK(repeated.full.len == sizeof(full_prefix) - 1 + + sizeof(marker) - 1 + 4); + CHECK(memcmp(repeated.full.data, full_prefix, + sizeof(full_prefix) - 1) == 0); + CHECK(memcmp(repeated.full.data + repeated.full.len - 4, + "NEXT", 4) == 0); + + memset(&input, 0, sizeof(input)); + input.reason = SYSTEM_EPOCH_START; + input.base.data = "KEEP"; + input.base.len = 4; + CHECK(system_epoch_build(&keep, &input) == 0); + exact.reason = SYSTEM_EPOCH_COMPACT; + exact.summary.data = "OLD"; + exact.summary.len = 3; + exact.full.data = "LEGACY-FULL-WRAPPER::OLD"; + exact.full.len = 24; + exact.restricted.data = "LEGACY-RESTRICTED-WRAPPER::OLD"; + exact.restricted.len = 30; + CHECK(system_epoch_import(&broken, &exact) == 0); + CHECK(system_epoch_replace_summary(&keep, &broken, &summary) == 0); + CHECK(buf_is(&keep.full, "LEGACY-FULL-WRAPPER::NEXT", 25)); + CHECK(buf_is(&keep.restricted, + "LEGACY-RESTRICTED-WRAPPER::NEXT", 31)); + + exact.full.data = "NO-SUFFIX"; + exact.full.len = 9; + CHECK(system_epoch_import(&keep, &exact) == -1); + CHECK(buf_is(&keep.full, "LEGACY-FULL-WRAPPER::NEXT", 25)); + + exact.version = SYSTEM_EPOCH_VERSION + 1; + CHECK(system_epoch_import(&keep, &exact) == -1); + CHECK(buf_is(&keep.full, "LEGACY-FULL-WRAPPER::NEXT", 25)); + + system_epoch_free(&broken); + system_epoch_free(&keep); + system_epoch_free(&repeated); + system_epoch_free(&compacted); + system_epoch_free(©); + system_epoch_free(&imported); +} + int main(void) { @@ -279,6 +391,7 @@ main(void) test_owned_state(); test_invalid_input_is_transactional(); test_all_reasons_and_binary_summary(); - test_copy_failure_is_transactional(); + test_copy_is_exact_and_transactional(); + test_exact_import_and_summary_replacement(); REGRESS_END(); } blob - ce8376919a1290d045d04e1b91fd5519fc35e8c2 blob + 1f11d8cd77d3a5323bc474977068436ff804b1e9 --- src/fugu/coord.c +++ src/fugu/coord.c @@ -4256,7 +4256,7 @@ compact_projection(struct coord *c, int active, int fo struct generation_result generation; struct generation_span text; struct system_epoch epoch; - struct system_epoch_input input; + struct system_epoch_span replacement_summary; struct turn_txn_projection projection; struct msglist compact_messages; struct msg *document_message; @@ -4370,17 +4370,10 @@ compact_projection(struct coord *c, int active, int fo } generation_result_dispose(&generation); - memset(&input, 0, sizeof(input)); - input.reason = SYSTEM_EPOCH_COMPACT; - input.base.data = c->epoch.base.data; - input.base.len = c->epoch.base.len; - input.personal.data = c->epoch.personal.data; - input.personal.len = c->epoch.personal.len; - input.project.data = c->epoch.project.data; - input.project.len = c->epoch.project.len; - input.summary.data = summary.data; - input.summary.len = summary.len; - if (system_epoch_build(&epoch, &input) == -1) { + replacement_summary.data = summary.data; + replacement_summary.len = summary.len; + if (system_epoch_replace_summary(&epoch, &c->epoch, + &replacement_summary) == -1) { why = "replacement System epoch exceeds its bound"; goto fail; } blob - 0c567c4088e25bf7ef84c288e1837ec1e06faaf1 blob + c1869d105447dd1b325f4aa61f1177841eade438 --- src/fugu/journal.c +++ src/fugu/journal.c @@ -1285,13 +1285,6 @@ byte_cmp(const void *ap, size_t alen, const void *bp, } static int -buf_same(const struct buf *a, const struct buf *b) -{ - return (a->len == b->len && - (a->len == 0 || memcmp(a->data, b->data, a->len) == 0)); -} - -static int parse_file_array(const struct json *jd, int tok, enum compaction_file_kind kind, struct compaction_files *files) { @@ -1512,7 +1505,7 @@ static int parse_epoch_record(const struct json *jd, int root, struct parsed_epoch *out) { struct system_epoch candidate; - struct system_epoch_input input; + struct system_epoch_exact_input input; struct buf base, personal, project, summary, full, restricted; char *key; size_t keylen; @@ -1569,13 +1562,10 @@ parse_epoch_record(const struct json *jd, int root, st decode_hex(jd, field[8], &full, SYSTEM_EPOCH_VARIANT_MAX) == -1 || decode_hex(jd, field[9], &restricted, - SYSTEM_EPOCH_VARIANT_MAX) == -1 || - (base.len > 0 && memchr(base.data, '\0', base.len) != NULL) || - (personal.len > 0 && memchr(personal.data, '\0', personal.len) - != NULL) || - (project.len > 0 && memchr(project.data, '\0', project.len) != NULL)) + SYSTEM_EPOCH_VARIANT_MAX) == -1) goto done; memset(&input, 0, sizeof(input)); + input.version = SYSTEM_EPOCH_VERSION; input.reason = out->epoch.reason; input.base.data = base.data; input.base.len = base.len; @@ -1585,9 +1575,11 @@ parse_epoch_record(const struct json *jd, int root, st input.project.len = project.len; input.summary.data = summary.data; input.summary.len = summary.len; - if (system_epoch_build(&candidate, &input) == -1 || - !buf_same(&candidate.full, &full) || - !buf_same(&candidate.restricted, &restricted)) + input.full.data = full.data; + input.full.len = full.len; + input.restricted.data = restricted.data; + input.restricted.len = restricted.len; + if (system_epoch_import(&candidate, &input) == -1) goto done; system_epoch_free(&out->epoch); out->epoch = candidate; blob - 14e9c5e26ee7067784bd3d946062195c7e02ef71 blob + 0060d2b27f01fe8c8f8ab2d40b2502b13cf3910b --- src/fugu/system_epoch.c +++ src/fugu/system_epoch.c @@ -49,17 +49,35 @@ size_add(size_t *total, size_t add) } static int +components_valid(enum system_epoch_reason reason, + const struct system_epoch_span *base, + const struct system_epoch_span *personal, + const struct system_epoch_span *project, + const struct system_epoch_span *summary) +{ + if (!reason_valid(reason) || !span_valid(base) || + !span_valid(personal) || !span_valid(project) || + !span_valid(summary) || base->len > SYSTEM_EPOCH_BASE_MAX || + personal->len > SYSTEM_EPOCH_EXTERNAL_MAX || + project->len > SYSTEM_EPOCH_EXTERNAL_MAX || + summary->len > SYSTEM_EPOCH_SUMMARY_MAX) + return (0); + if ((base->len > 0 && memchr(base->data, '\0', base->len) != NULL) || + (personal->len > 0 && memchr(personal->data, '\0', + personal->len) != NULL) || + (project->len > 0 && memchr(project->data, '\0', + project->len) != NULL)) + return (0); + return (1); +} + +static int input_valid(const struct system_epoch_input *input) { size_t full, restricted, summary = 0; - if (input == NULL || !reason_valid(input->reason) || - !span_valid(&input->base) || !span_valid(&input->personal) || - !span_valid(&input->project) || !span_valid(&input->summary) || - input->base.len > SYSTEM_EPOCH_BASE_MAX || - input->personal.len > SYSTEM_EPOCH_EXTERNAL_MAX || - input->project.len > SYSTEM_EPOCH_EXTERNAL_MAX || - input->summary.len > SYSTEM_EPOCH_SUMMARY_MAX) + if (input == NULL || !components_valid(input->reason, &input->base, + &input->personal, &input->project, &input->summary)) return (0); full = restricted = input->base.len; if (input->personal.len > 0 && @@ -80,13 +98,29 @@ input_valid(const struct system_epoch_input *input) if (full > SYSTEM_EPOCH_VARIANT_MAX || restricted > SYSTEM_EPOCH_VARIANT_MAX) return (0); - if ((input->base.len > 0 && memchr(input->base.data, '\0', - input->base.len) != NULL) || - (input->personal.len > 0 && memchr(input->personal.data, '\0', - input->personal.len) != NULL) || - (input->project.len > 0 && memchr(input->project.data, '\0', - input->project.len) != NULL)) + return (1); +} + +static int +exact_input_valid(const struct system_epoch_exact_input *input) +{ + if (input == NULL || input->version != SYSTEM_EPOCH_VERSION || + !components_valid(input->reason, &input->base, &input->personal, + &input->project, &input->summary) || !span_valid(&input->full) || + !span_valid(&input->restricted) || + input->full.len > SYSTEM_EPOCH_VARIANT_MAX || + input->restricted.len > SYSTEM_EPOCH_VARIANT_MAX) return (0); + if (input->summary.len > 0 && + (input->full.len < input->summary.len || + input->restricted.len < input->summary.len || + memcmp((const u_char *)input->full.data + input->full.len - + input->summary.len, input->summary.data, + input->summary.len) != 0 || + memcmp((const u_char *)input->restricted.data + + input->restricted.len - input->summary.len, + input->summary.data, input->summary.len) != 0)) + return (0); return (1); } @@ -115,13 +149,6 @@ add_summary(struct buf *out, const struct system_epoch add_span(out, summary); } -static int -buf_equal(const struct buf *a, const struct buf *b) -{ - return (a->len == b->len && - (a->len == 0 || memcmp(a->data, b->data, a->len) == 0)); -} - void system_epoch_init(struct system_epoch *epoch) { @@ -176,37 +203,132 @@ system_epoch_build(struct system_epoch *epoch, } int +system_epoch_import(struct system_epoch *epoch, + const struct system_epoch_exact_input *input) +{ + struct system_epoch next; + + if (epoch == NULL || !exact_input_valid(input)) + return (-1); + system_epoch_init(&next); + next.version = input->version; + next.reason = input->reason; + add_span(&next.base, &input->base); + add_span(&next.personal, &input->personal); + add_span(&next.project, &input->project); + add_span(&next.summary, &input->summary); + add_span(&next.full, &input->full); + add_span(&next.restricted, &input->restricted); + system_epoch_free(epoch); + *epoch = next; + return (0); +} + +static void +exact_input_from_epoch(struct system_epoch_exact_input *input, + const struct system_epoch *epoch) +{ + memset(input, 0, sizeof(*input)); + input->version = epoch->version; + input->reason = epoch->reason; + input->base.data = epoch->base.data; + input->base.len = epoch->base.len; + input->personal.data = epoch->personal.data; + input->personal.len = epoch->personal.len; + input->project.data = epoch->project.data; + input->project.len = epoch->project.len; + input->summary.data = epoch->summary.data; + input->summary.len = epoch->summary.len; + input->full.data = epoch->full.data; + input->full.len = epoch->full.len; + input->restricted.data = epoch->restricted.data; + input->restricted.len = epoch->restricted.len; +} + +int system_epoch_copy(struct system_epoch *dst, const struct system_epoch *src) { - struct system_epoch_input input; - struct system_epoch next; + struct system_epoch_exact_input input; - if (dst == NULL || src == NULL || src->version != SYSTEM_EPOCH_VERSION) + if (dst == NULL || src == NULL) return (-1); - if (dst == src) + exact_input_from_epoch(&input, src); + return (system_epoch_import(dst, &input)); +} + +static int +variant_prefix_len(const struct system_epoch *epoch, + const struct buf *variant, size_t *prefix_len) +{ + if (epoch->summary.len == 0) { + *prefix_len = variant->len; return (0); - memset(&input, 0, sizeof(input)); - input.reason = src->reason; - input.base.data = src->base.data; - input.base.len = src->base.len; - input.personal.data = src->personal.data; - input.personal.len = src->personal.len; - input.project.data = src->project.data; - input.project.len = src->project.len; - input.summary.data = src->summary.data; - input.summary.len = src->summary.len; - system_epoch_init(&next); - if (system_epoch_build(&next, &input) == -1 || - !buf_equal(&next.full, &src->full) || - !buf_equal(&next.restricted, &src->restricted)) { - system_epoch_free(&next); - return (-1); } - system_epoch_free(dst); - *dst = next; + if (variant->len < epoch->summary.len) + return (-1); + *prefix_len = variant->len - epoch->summary.len; + if (memcmp(variant->data + *prefix_len, epoch->summary.data, + epoch->summary.len) != 0) + return (-1); return (0); } +int +system_epoch_replace_summary(struct system_epoch *dst, + const struct system_epoch *src, const struct system_epoch_span *summary) +{ + struct system_epoch_exact_input source, exact; + struct buf full, restricted; + size_t full_prefix, restricted_prefix, suffix_len = 0; + int rc = -1; + + if (dst == NULL || src == NULL || summary == NULL || + !span_valid(summary) || summary->len == 0 || + summary->len > SYSTEM_EPOCH_SUMMARY_MAX) + return (-1); + exact_input_from_epoch(&source, src); + if (!exact_input_valid(&source) || + variant_prefix_len(src, &src->full, &full_prefix) == -1 || + variant_prefix_len(src, &src->restricted, + &restricted_prefix) == -1) + return (-1); + if (src->summary.len == 0) { + suffix_len = sizeof(summary_prefix) - 1; + if (size_add(&suffix_len, summary->len) == -1) + return (-1); + } else + suffix_len = summary->len; + if (suffix_len > SYSTEM_EPOCH_VARIANT_MAX || + full_prefix > SYSTEM_EPOCH_VARIANT_MAX - suffix_len || + restricted_prefix > SYSTEM_EPOCH_VARIANT_MAX - suffix_len) + return (-1); + + buf_init(&full); + buf_init(&restricted); + if (full_prefix > 0) + buf_add(&full, src->full.data, full_prefix); + if (restricted_prefix > 0) + buf_add(&restricted, src->restricted.data, restricted_prefix); + if (src->summary.len == 0) { + add_summary(&full, summary); + add_summary(&restricted, summary); + } else { + add_span(&full, summary); + add_span(&restricted, summary); + } + exact = source; + exact.reason = SYSTEM_EPOCH_COMPACT; + exact.summary = *summary; + exact.full.data = full.data; + exact.full.len = full.len; + exact.restricted.data = restricted.data; + exact.restricted.len = restricted.len; + rc = system_epoch_import(dst, &exact); + buf_free(&restricted); + buf_free(&full); + return (rc); +} + const struct buf * system_epoch_select(const struct system_epoch *epoch, int project_context) { blob - cfe01a1a5d4bb697e413f1e2ed212a8a04a603fa blob + be859a0ed0a34709c9d0e954728c4a496838075c --- src/fugu/system_epoch.h +++ src/fugu/system_epoch.h @@ -51,12 +51,30 @@ struct system_epoch_input { }; /* + * Exact persisted form. The complete variants are authoritative opaque + * Provider inputs; components support audit, Subagents, and future reset + * construction, but are not hashes from which replay may rebuild a variant. + */ +struct system_epoch_exact_input { + uint32_t version; + enum system_epoch_reason reason; + struct system_epoch_span base; + struct system_epoch_span personal; + struct system_epoch_span project; + struct system_epoch_span summary; + struct system_epoch_span full; + struct system_epoch_span restricted; +}; + +/* * One owned immutable System baseline epoch. base, personal, and project - * are NUL-free; summary and the derived variants may contain NUL. All spans + * are NUL-free; summary and the complete variants may contain NUL. All spans * are otherwise opaque bytes: this Module deliberately does not validate or * normalize UTF-8. Each external span is capped at FUGU_CTX_MAX, summary at * FUGU_TURN_DATA_MAX, and base and either complete variant at - * FUGU_REQUEST_MAX. A Provider codec still checks final JSON expansion. + * FUGU_REQUEST_MAX. Fresh construction derives variants; import and copy + * preserve their exact bytes. A Provider codec still checks final JSON + * expansion. */ struct system_epoch { uint32_t version; @@ -74,7 +92,13 @@ void system_epoch_free(struct system_epoch *); /* On failure, a previously built destination remains byte-identical. */ int system_epoch_build(struct system_epoch *, const struct system_epoch_input *); +/* Import all authoritative spans without recomposing either complete variant. */ +int system_epoch_import(struct system_epoch *, + const struct system_epoch_exact_input *); int system_epoch_copy(struct system_epoch *, const struct system_epoch *); +/* Preserve both exact variant prefixes while replacing their summary suffix. */ +int system_epoch_replace_summary(struct system_epoch *, + const struct system_epoch *, const struct system_epoch_span *); /* Borrowed until the epoch is rebuilt or freed. */ const struct buf *system_epoch_select(const struct system_epoch *, int);