@awebai/oats 0.41.1 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,9 +86,65 @@ export function manifestContractProblems(m) {
86
86
  else if (isObject(value) && value.required === true && event !== "spawn") bad(`${at}/required`, `hook "${event}" cannot be required — only the spawn hook is enforced (retire, launch and soul-scaffold run outside a spawn transaction)`);
87
87
  if (hookScriptEscapes(command)) bad(isObject(value) ? `${at}/command` : at, `hook "${event}" script ${JSON.stringify(command.trim().split(/\s+/)[0])} escapes the capability directory; a hook script is a path inside it`);
88
88
  }
89
+
90
+ if (m.retirement !== undefined) {
91
+ if (!isObject(m.retirement) || !isObject(m.retirement.disposable)) bad("/retirement", "manifest retirement must contain a disposable map");
92
+ else {
93
+ const unknown = Object.keys(m.retirement).filter((key) => key !== "disposable");
94
+ const scopes = Object.keys(m.retirement.disposable).filter((key) => !["home", "work"].includes(key));
95
+ if (unknown.length || scopes.length) bad("/retirement", `manifest retirement has unsupported keys: ${[...unknown, ...scopes].join(", ")}`);
96
+ for (const scope of ["home", "work"]) {
97
+ const roots = m.retirement.disposable[scope];
98
+ if (roots === undefined) continue;
99
+ const at = `/retirement/disposable/${scope}`;
100
+ if (!Array.isArray(roots) || roots.some((root) => typeof root !== "string")) { bad(at, `manifest retirement.disposable.${scope} must be an array of relative roots`); continue; }
101
+ if (scope !== "home") continue;
102
+ roots.forEach((root, i) => {
103
+ const problem = disposableHomeRootProblem(root);
104
+ if (problem === "shape") bad(`${at}/${i}`, `manifest retirement.disposable.home entry ${JSON.stringify(root)} must name one hidden top-level home entry (".name", or ".prefix-*")`);
105
+ else if (problem === "kernel-owned") bad(`${at}/${i}`, `manifest retirement.disposable.home entry ${JSON.stringify(root)} covers a kernel-owned home entry`);
106
+ });
107
+ }
108
+ }
109
+ }
89
110
  return problems;
90
111
  }
91
112
 
113
+ /** `retirement.disposable.home`: provider-owned top-level entries of an instance
114
+ * home that retirement neither fingerprints nor copies to recovery. An entry is
115
+ * one hidden top-level name (`.aw`) or a prefix (`.aweb-identity-*`: every
116
+ * top-level name starting with the text before `*`). No separator, no
117
+ * traversal, no other glob, and nothing that covers a name the kernel owns in
118
+ * a home. The published schema (docs/capability-manifest.schema.json) states
119
+ * the same grammar. */
120
+ const DISPOSABLE_HOME_EXACT_RE = /^\.[A-Za-z0-9_][A-Za-z0-9._-]*$/;
121
+ const DISPOSABLE_HOME_PREFIX_RE = /^\.[A-Za-z0-9_][A-Za-z0-9._-]*-\*$/;
122
+ /** The top-level entries the KERNEL writes in an instance home, as a
123
+ * declaration may not cover them: these exact names, and every exact name
124
+ * starting with one of these prefixes (the events log; the stop, restart and
125
+ * start receipts and locks; the rollback marker; AGENTS.md backups; the
126
+ * attachments directory). This is the one list: a new kernel-written top-level
127
+ * home entry MUST be added here and to the `not` patterns of
128
+ * `retirement.disposable.home` in docs/capability-manifest.schema.json. A
129
+ * prefix declaration can reach none of them except through `.oats-`, which is
130
+ * refused whole; `.oats-<provider>` as an exact name stays declarable. */
131
+ const KERNEL_HOME_NAMES = new Set([".oats", ".agents", ".claude"]);
132
+ const KERNEL_HOME_NAME_PREFIXES = [".oats-events", ".oats-stop", ".oats-restart", ".oats-rollback", ".oats-agents-md", ".oats-start", ".oats-attachments"];
133
+
134
+ /** Why `root` is not a declarable home entry: "shape" (not one hidden top-level
135
+ * name or prefix), "kernel-owned", or undefined when it is sound. */
136
+ export function disposableHomeRootProblem(root) {
137
+ if (typeof root !== "string") return "shape";
138
+ if (DISPOSABLE_HOME_PREFIX_RE.test(root)) return root.startsWith(".oats-") ? "kernel-owned" : undefined;
139
+ if (!DISPOSABLE_HOME_EXACT_RE.test(root)) return "shape";
140
+ return KERNEL_HOME_NAMES.has(root) || KERNEL_HOME_NAME_PREFIXES.some((prefix) => root.startsWith(prefix)) ? "kernel-owned" : undefined;
141
+ }
142
+
143
+ /** Whether the declared `root` (already sound) covers the top-level home entry `name`. */
144
+ export function disposableHomeRootMatches(root, name) {
145
+ return root.endsWith("*") ? name.startsWith(root.slice(0, -1)) : name === root;
146
+ }
147
+
92
148
  /** The setting keys a manifest declares (`settings.<key>`), sorted: names only, never their
93
149
  * descriptions or defaults. The spawn preview and inspect expose them so a client can gate
94
150
  * a choice on a declared key (e.g. a messaging provider's `join`). */