@abaigram/abai-stub 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/audit.d.ts +70 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +166 -0
- package/dist/audit.js.map +1 -0
- package/dist/cells/fit.d.ts +6 -0
- package/dist/cells/fit.d.ts.map +1 -0
- package/dist/cells/fit.js +44 -0
- package/dist/cells/fit.js.map +1 -0
- package/dist/content.d.ts +17 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +71 -0
- package/dist/content.js.map +1 -0
- package/dist/endpoint.d.ts +90 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +789 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/entry.d.ts +36 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +409 -0
- package/dist/entry.js.map +1 -0
- package/dist/env.d.ts +38 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +69 -0
- package/dist/env.js.map +1 -0
- package/dist/harness-binding.d.ts +223 -0
- package/dist/harness-binding.d.ts.map +1 -0
- package/dist/harness-binding.js +589 -0
- package/dist/harness-binding.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/index.d.ts +3 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +3 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/manifest.d.ts +172 -0
- package/dist/manifest/manifest.d.ts.map +1 -0
- package/dist/manifest/manifest.js +443 -0
- package/dist/manifest/manifest.js.map +1 -0
- package/dist/manifest/render.d.ts +6 -0
- package/dist/manifest/render.d.ts.map +1 -0
- package/dist/manifest/render.js +39 -0
- package/dist/manifest/render.js.map +1 -0
- package/dist/null-harness.d.ts +32 -0
- package/dist/null-harness.d.ts.map +1 -0
- package/dist/null-harness.js +162 -0
- package/dist/null-harness.js.map +1 -0
- package/dist/program.d.ts +250 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +877 -0
- package/dist/program.js.map +1 -0
- package/dist/region.d.ts +72 -0
- package/dist/region.d.ts.map +1 -0
- package/dist/region.js +92 -0
- package/dist/region.js.map +1 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +101 -0
- package/dist/registry.js.map +1 -0
- package/dist/wire/descriptor.d.ts +38 -0
- package/dist/wire/descriptor.d.ts.map +1 -0
- package/dist/wire/descriptor.js +144 -0
- package/dist/wire/descriptor.js.map +1 -0
- package/dist/wire/framing.d.ts +11 -0
- package/dist/wire/framing.d.ts.map +1 -0
- package/dist/wire/framing.js +42 -0
- package/dist/wire/framing.js.map +1 -0
- package/dist/wire/index.d.ts +4 -0
- package/dist/wire/index.d.ts.map +1 -0
- package/dist/wire/index.js +4 -0
- package/dist/wire/index.js.map +1 -0
- package/dist/wire/protocol.d.ts +255 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +15 -0
- package/dist/wire/protocol.js.map +1 -0
- package/package.json +36 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joon Hwa Jung
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @abaigram/abai-stub
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The library a TypeScript program includes to become an abaigram, with
|
|
4
|
+
`npm install @abaigram/abai-stub @abaigram/abai-embedded-harness`: the
|
|
5
|
+
program side of the contract, `spec/abaigrammar.md` in the
|
|
6
|
+
[abai repository](https://github.com/jjbits/abai). Declarations in code become the
|
|
7
|
+
manifest; cells, events, capabilities, decisions, turns and safepoints are
|
|
8
|
+
the program's end of the five channels; `<prog> abai manifest | doctor`
|
|
9
|
+
is the stub's part of the standard entry.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createAbaigram } from "@abaigram/abai-stub";
|
|
13
|
+
|
|
14
|
+
const app = createAbaigram({ name: "counter", modes: ["turns", "sidecar"], cells: {...}, ... });
|
|
15
|
+
if ((await app.entry(process.argv.slice(2))) >= 0) process.exit();
|
|
16
|
+
const depth = app.cell<number>("queue.depth"); // obtain handles once; names are checked here
|
|
17
|
+
const failed = app.event("job.failed");
|
|
18
|
+
depth.set(3); // one store
|
|
19
|
+
await app.decide("on_backlog", { depth: 3 }); // the default, until a harness attaches
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
In the repository, `implementations/typescript/abai-stub/examples/counter/` is
|
|
23
|
+
the smallest abaigram and the test subject.
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type DecisionDecl } from "./manifest/manifest.ts";
|
|
2
|
+
export type AuditAct = "capability" | "knob" | "decision" | "turn" | "lifecycle" | "counters";
|
|
3
|
+
export type AuditOutcome = "ok" | "error" | "forbidden" | "agent" | "default" | "stub" | "failed";
|
|
4
|
+
export interface AuditRecord {
|
|
5
|
+
ts: string;
|
|
6
|
+
pid: number;
|
|
7
|
+
/** The connection that acted (`k1`, `k2`, ...); absent for counters. */
|
|
8
|
+
conn?: string;
|
|
9
|
+
act: AuditAct;
|
|
10
|
+
/** The capability, cell, decision or counter name. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The request id or hook id. */
|
|
13
|
+
id?: string;
|
|
14
|
+
/** The parameters (capped, see `capValue`). */
|
|
15
|
+
params?: unknown;
|
|
16
|
+
/** A knob's landed value, or a decision answer's summary. */
|
|
17
|
+
value?: unknown;
|
|
18
|
+
outcome: AuditOutcome;
|
|
19
|
+
/** Why a default or stub answer applied, or what a counter counts. */
|
|
20
|
+
cause?: string;
|
|
21
|
+
/** What the harness stated for an act whose policy is `confirm`: approved, auto, or denied. */
|
|
22
|
+
consent?: string;
|
|
23
|
+
/** The agent's stated reason. */
|
|
24
|
+
reason?: unknown;
|
|
25
|
+
error?: unknown;
|
|
26
|
+
/** Milliseconds from dispatch to settle. */
|
|
27
|
+
ms?: number;
|
|
28
|
+
/** Counters only. */
|
|
29
|
+
count?: number;
|
|
30
|
+
}
|
|
31
|
+
/** Bytes a string or the JSON of a value may take in a record before it is cut. */
|
|
32
|
+
export declare const AUDIT_VALUE_CAP = 512;
|
|
33
|
+
/** Bytes `tail` reads from the end of the file. */
|
|
34
|
+
export declare const AUDIT_TAIL_BYTES: number;
|
|
35
|
+
/**
|
|
36
|
+
* Cap a value for a record: a value whose JSON exceeds the cap becomes
|
|
37
|
+
* `{truncated: true, bytes, head}` with the JSON's first 512 characters, so
|
|
38
|
+
* the record stays valid JSON. A value that cannot be serialized becomes a
|
|
39
|
+
* marker string.
|
|
40
|
+
*/
|
|
41
|
+
export declare function capValue(value: unknown): unknown;
|
|
42
|
+
/**
|
|
43
|
+
* What an audit record keeps of a decision answer: never the transcript.
|
|
44
|
+
* An options decision keeps the chosen option; any other answer keeps
|
|
45
|
+
* whether it changed something (a non-empty object).
|
|
46
|
+
*/
|
|
47
|
+
export declare function summarizeAnswer(decision: DecisionDecl | undefined, answer: unknown): unknown;
|
|
48
|
+
export interface AuditTail {
|
|
49
|
+
records: unknown[];
|
|
50
|
+
/** More records exist than were returned. */
|
|
51
|
+
truncated: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare class AuditLog {
|
|
54
|
+
readonly path: string;
|
|
55
|
+
private readonly onFailure;
|
|
56
|
+
private fd;
|
|
57
|
+
private _failed;
|
|
58
|
+
private closed;
|
|
59
|
+
constructor(path: string, onFailure?: (error: string) => void);
|
|
60
|
+
/** The error that latched the log off, if any. */
|
|
61
|
+
get failed(): string | undefined;
|
|
62
|
+
/** Append one record. Never throws; a no-op once the log failed or was closed. */
|
|
63
|
+
append(record: Omit<AuditRecord, "ts" | "pid">): void;
|
|
64
|
+
private fail;
|
|
65
|
+
/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on("exit")`. */
|
|
66
|
+
close(): void;
|
|
67
|
+
/** The last `limit` records, read from the last megabyte of the file. Never throws. */
|
|
68
|
+
tail(limit: number): AuditTail;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAWA,OAAO,EAAW,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEpE,MAAM,MAAM,QAAQ,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;AAC9F,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAElG,MAAM,WAAW,WAAW;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,QAAQ,CAAC;IACd,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4CAA4C;IAC5C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,mFAAmF;AACnF,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,mDAAmD;AACnD,eAAO,MAAM,gBAAgB,QAAc,CAAC;AAM5C;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWhD;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAI5F;AAED,MAAM,WAAW,SAAS;IACzB,OAAO,EAAE,OAAO,EAAE,CAAC;IACnB,6CAA6C;IAC7C,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,QAAQ;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,OAAO,CAAC,EAAE,CAAqB;IAC/B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,MAAM,CAAS;IAEvB,YAAY,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAG5D;IAED,kDAAkD;IAClD,IAAI,MAAM,IAAI,MAAM,GAAG,SAAS,CAE/B;IAED,kFAAkF;IAClF,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,KAAK,CAAC,GAAG,IAAI,CAYpD;IAED,OAAO,CAAC,IAAI;IAUZ,0GAA0G;IAC1G,KAAK,IAAI,IAAI,CAUZ;IAED,uFAAuF;IACvF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CA6C7B;CACD","sourcesContent":["// The audit log (abaigrammar section 10).\n//\n// One JSON line per act of control: a mutate capability call, a knob write\n// by any path, a decision's outcome, and the counters of defaulted decisions\n// flushed at attach, close and exit. Written synchronously, one line per\n// act: acts are rare and a partial line on a crash is worse than a syscall.\n// Nothing here throws into the agent: the first failure latches the log off,\n// reports once through `onFailure`, and every later append is a no-op.\n// `tail` reads the log back for the harness.\nimport { closeSync, fstatSync, mkdirSync, openSync, readSync, writeSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { cutUtf8, type DecisionDecl } from \"./manifest/manifest.ts\";\n\nexport type AuditAct = \"capability\" | \"knob\" | \"decision\" | \"turn\" | \"lifecycle\" | \"counters\";\nexport type AuditOutcome = \"ok\" | \"error\" | \"forbidden\" | \"agent\" | \"default\" | \"stub\" | \"failed\";\n\nexport interface AuditRecord {\n\tts: string;\n\tpid: number;\n\t/** The connection that acted (`k1`, `k2`, ...); absent for counters. */\n\tconn?: string;\n\tact: AuditAct;\n\t/** The capability, cell, decision or counter name. */\n\tname: string;\n\t/** The request id or hook id. */\n\tid?: string;\n\t/** The parameters (capped, see `capValue`). */\n\tparams?: unknown;\n\t/** A knob's landed value, or a decision answer's summary. */\n\tvalue?: unknown;\n\toutcome: AuditOutcome;\n\t/** Why a default or stub answer applied, or what a counter counts. */\n\tcause?: string;\n\t/** What the harness stated for an act whose policy is `confirm`: approved, auto, or denied. */\n\tconsent?: string;\n\t/** The agent's stated reason. */\n\treason?: unknown;\n\terror?: unknown;\n\t/** Milliseconds from dispatch to settle. */\n\tms?: number;\n\t/** Counters only. */\n\tcount?: number;\n}\n\n/** Bytes a string or the JSON of a value may take in a record before it is cut. */\nexport const AUDIT_VALUE_CAP = 512;\n/** Bytes `tail` reads from the end of the file. */\nexport const AUDIT_TAIL_BYTES = 1024 * 1024;\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Cap a value for a record: a value whose JSON exceeds the cap becomes\n * `{truncated: true, bytes, head}` with the JSON's first 512 characters, so\n * the record stays valid JSON. A value that cannot be serialized becomes a\n * marker string.\n */\nexport function capValue(value: unknown): unknown {\n\tif (value === undefined) return undefined;\n\tlet json: string;\n\ttry {\n\t\tjson = JSON.stringify(value) ?? \"null\";\n\t} catch (error) {\n\t\treturn `<unserializable: ${errorMessage(error)}>`;\n\t}\n\tconst bytes = Buffer.byteLength(json, \"utf8\");\n\tif (bytes <= AUDIT_VALUE_CAP) return value;\n\treturn { truncated: true, bytes, head: cutUtf8(json, AUDIT_VALUE_CAP) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * What an audit record keeps of a decision answer: never the transcript.\n * An options decision keeps the chosen option; any other answer keeps\n * whether it changed something (a non-empty object).\n */\nexport function summarizeAnswer(decision: DecisionDecl | undefined, answer: unknown): unknown {\n\tif (answer === undefined) return undefined;\n\tif (decision?.options && typeof answer === \"string\") return { option: answer };\n\treturn { changed: Object.keys(isRecord(answer) ? answer : {}).length > 0 };\n}\n\nexport interface AuditTail {\n\trecords: unknown[];\n\t/** More records exist than were returned. */\n\ttruncated: boolean;\n}\n\nexport class AuditLog {\n\treadonly path: string;\n\tprivate readonly onFailure: ((error: string) => void) | undefined;\n\tprivate fd: number | undefined;\n\tprivate _failed: string | undefined;\n\tprivate closed = false;\n\n\tconstructor(path: string, onFailure?: (error: string) => void) {\n\t\tthis.path = path;\n\t\tthis.onFailure = onFailure;\n\t}\n\n\t/** The error that latched the log off, if any. */\n\tget failed(): string | undefined {\n\t\treturn this._failed;\n\t}\n\n\t/** Append one record. Never throws; a no-op once the log failed or was closed. */\n\tappend(record: Omit<AuditRecord, \"ts\" | \"pid\">): void {\n\t\tif (this._failed !== undefined || this.closed) return;\n\t\ttry {\n\t\t\tif (this.fd === undefined) {\n\t\t\t\tmkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });\n\t\t\t\tthis.fd = openSync(this.path, \"a\", 0o600);\n\t\t\t}\n\t\t\tconst full: AuditRecord = { ts: new Date().toISOString(), pid: process.pid, ...record };\n\t\t\twriteSync(this.fd, `${JSON.stringify(full)}\\n`);\n\t\t} catch (error) {\n\t\t\tthis.fail(errorMessage(error));\n\t\t}\n\t}\n\n\tprivate fail(error: string): void {\n\t\tthis._failed = error;\n\t\tthis.close();\n\t\ttry {\n\t\t\tthis.onFailure?.(error);\n\t\t} catch {\n\t\t\t// a failure callback must not fail the caller\n\t\t}\n\t}\n\n\t/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on(\"exit\")`. */\n\tclose(): void {\n\t\tthis.closed = true;\n\t\tif (this.fd === undefined) return;\n\t\tconst fd = this.fd;\n\t\tthis.fd = undefined;\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\t/** The last `limit` records, read from the last megabyte of the file. Never throws. */\n\ttail(limit: number): AuditTail {\n\t\tlet fd: number | undefined;\n\t\ttry {\n\t\t\tfd = openSync(this.path, \"r\");\n\t\t\tconst size = fstatSync(fd).size;\n\t\t\tconst start = Math.max(0, size - AUDIT_TAIL_BYTES);\n\t\t\tconst buffer = Buffer.alloc(size - start);\n\t\t\tlet read = 0;\n\t\t\twhile (read < buffer.length) {\n\t\t\t\tconst n = readSync(fd, buffer, read, buffer.length - read, start + read);\n\t\t\t\tif (n === 0) break;\n\t\t\t\tread += n;\n\t\t\t}\n\t\t\tconst lines = buffer.toString(\"utf8\", 0, read).split(\"\\n\");\n\t\t\t// the window may start mid-line: drop the first line unless the byte before it ends a line\n\t\t\tif (start > 0) {\n\t\t\t\tconst before = Buffer.alloc(1);\n\t\t\t\tconst n = readSync(fd, before, 0, 1, start - 1);\n\t\t\t\tif (n !== 1 || before[0] !== 0x0a) lines.shift();\n\t\t\t}\n\t\t\tconst records: unknown[] = [];\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line === \"\") continue;\n\t\t\t\ttry {\n\t\t\t\t\trecords.push(JSON.parse(line));\n\t\t\t\t} catch {\n\t\t\t\t\trecords.push({ raw: line });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst truncated = start > 0 || records.length > limit;\n\t\t\treturn { records: records.slice(-limit), truncated };\n\t\t} catch (error) {\n\t\t\t// no log yet, or a path that can never hold one (the failure is in `failed`)\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"ENOENT\" || code === \"ENOTDIR\") return { records: [], truncated: false };\n\t\t\treturn { records: [{ error: errorMessage(error) }], truncated: false };\n\t\t} finally {\n\t\t\tif (fd !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// The audit log (abaigrammar section 10).
|
|
2
|
+
//
|
|
3
|
+
// One JSON line per act of control: a mutate capability call, a knob write
|
|
4
|
+
// by any path, a decision's outcome, and the counters of defaulted decisions
|
|
5
|
+
// flushed at attach, close and exit. Written synchronously, one line per
|
|
6
|
+
// act: acts are rare and a partial line on a crash is worse than a syscall.
|
|
7
|
+
// Nothing here throws into the agent: the first failure latches the log off,
|
|
8
|
+
// reports once through `onFailure`, and every later append is a no-op.
|
|
9
|
+
// `tail` reads the log back for the harness.
|
|
10
|
+
import { closeSync, fstatSync, mkdirSync, openSync, readSync, writeSync } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { cutUtf8 } from "./manifest/manifest.js";
|
|
13
|
+
/** Bytes a string or the JSON of a value may take in a record before it is cut. */
|
|
14
|
+
export const AUDIT_VALUE_CAP = 512;
|
|
15
|
+
/** Bytes `tail` reads from the end of the file. */
|
|
16
|
+
export const AUDIT_TAIL_BYTES = 1024 * 1024;
|
|
17
|
+
function errorMessage(error) {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cap a value for a record: a value whose JSON exceeds the cap becomes
|
|
22
|
+
* `{truncated: true, bytes, head}` with the JSON's first 512 characters, so
|
|
23
|
+
* the record stays valid JSON. A value that cannot be serialized becomes a
|
|
24
|
+
* marker string.
|
|
25
|
+
*/
|
|
26
|
+
export function capValue(value) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
let json;
|
|
30
|
+
try {
|
|
31
|
+
json = JSON.stringify(value) ?? "null";
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return `<unserializable: ${errorMessage(error)}>`;
|
|
35
|
+
}
|
|
36
|
+
const bytes = Buffer.byteLength(json, "utf8");
|
|
37
|
+
if (bytes <= AUDIT_VALUE_CAP)
|
|
38
|
+
return value;
|
|
39
|
+
return { truncated: true, bytes, head: cutUtf8(json, AUDIT_VALUE_CAP) };
|
|
40
|
+
}
|
|
41
|
+
function isRecord(value) {
|
|
42
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What an audit record keeps of a decision answer: never the transcript.
|
|
46
|
+
* An options decision keeps the chosen option; any other answer keeps
|
|
47
|
+
* whether it changed something (a non-empty object).
|
|
48
|
+
*/
|
|
49
|
+
export function summarizeAnswer(decision, answer) {
|
|
50
|
+
if (answer === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (decision?.options && typeof answer === "string")
|
|
53
|
+
return { option: answer };
|
|
54
|
+
return { changed: Object.keys(isRecord(answer) ? answer : {}).length > 0 };
|
|
55
|
+
}
|
|
56
|
+
export class AuditLog {
|
|
57
|
+
path;
|
|
58
|
+
onFailure;
|
|
59
|
+
fd;
|
|
60
|
+
_failed;
|
|
61
|
+
closed = false;
|
|
62
|
+
constructor(path, onFailure) {
|
|
63
|
+
this.path = path;
|
|
64
|
+
this.onFailure = onFailure;
|
|
65
|
+
}
|
|
66
|
+
/** The error that latched the log off, if any. */
|
|
67
|
+
get failed() {
|
|
68
|
+
return this._failed;
|
|
69
|
+
}
|
|
70
|
+
/** Append one record. Never throws; a no-op once the log failed or was closed. */
|
|
71
|
+
append(record) {
|
|
72
|
+
if (this._failed !== undefined || this.closed)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
if (this.fd === undefined) {
|
|
76
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
77
|
+
this.fd = openSync(this.path, "a", 0o600);
|
|
78
|
+
}
|
|
79
|
+
const full = { ts: new Date().toISOString(), pid: process.pid, ...record };
|
|
80
|
+
writeSync(this.fd, `${JSON.stringify(full)}\n`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.fail(errorMessage(error));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
fail(error) {
|
|
87
|
+
this._failed = error;
|
|
88
|
+
this.close();
|
|
89
|
+
try {
|
|
90
|
+
this.onFailure?.(error);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// a failure callback must not fail the caller
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on("exit")`. */
|
|
97
|
+
close() {
|
|
98
|
+
this.closed = true;
|
|
99
|
+
if (this.fd === undefined)
|
|
100
|
+
return;
|
|
101
|
+
const fd = this.fd;
|
|
102
|
+
this.fd = undefined;
|
|
103
|
+
try {
|
|
104
|
+
closeSync(fd);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// ignore
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The last `limit` records, read from the last megabyte of the file. Never throws. */
|
|
111
|
+
tail(limit) {
|
|
112
|
+
let fd;
|
|
113
|
+
try {
|
|
114
|
+
fd = openSync(this.path, "r");
|
|
115
|
+
const size = fstatSync(fd).size;
|
|
116
|
+
const start = Math.max(0, size - AUDIT_TAIL_BYTES);
|
|
117
|
+
const buffer = Buffer.alloc(size - start);
|
|
118
|
+
let read = 0;
|
|
119
|
+
while (read < buffer.length) {
|
|
120
|
+
const n = readSync(fd, buffer, read, buffer.length - read, start + read);
|
|
121
|
+
if (n === 0)
|
|
122
|
+
break;
|
|
123
|
+
read += n;
|
|
124
|
+
}
|
|
125
|
+
const lines = buffer.toString("utf8", 0, read).split("\n");
|
|
126
|
+
// the window may start mid-line: drop the first line unless the byte before it ends a line
|
|
127
|
+
if (start > 0) {
|
|
128
|
+
const before = Buffer.alloc(1);
|
|
129
|
+
const n = readSync(fd, before, 0, 1, start - 1);
|
|
130
|
+
if (n !== 1 || before[0] !== 0x0a)
|
|
131
|
+
lines.shift();
|
|
132
|
+
}
|
|
133
|
+
const records = [];
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (line === "")
|
|
136
|
+
continue;
|
|
137
|
+
try {
|
|
138
|
+
records.push(JSON.parse(line));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
records.push({ raw: line });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const truncated = start > 0 || records.length > limit;
|
|
145
|
+
return { records: records.slice(-limit), truncated };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// no log yet, or a path that can never hold one (the failure is in `failed`)
|
|
149
|
+
const code = error.code;
|
|
150
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
151
|
+
return { records: [], truncated: false };
|
|
152
|
+
return { records: [{ error: errorMessage(error) }], truncated: false };
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (fd !== undefined) {
|
|
156
|
+
try {
|
|
157
|
+
closeSync(fd);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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","sourcesContent":["// The audit log (abaigrammar section 10).\n//\n// One JSON line per act of control: a mutate capability call, a knob write\n// by any path, a decision's outcome, and the counters of defaulted decisions\n// flushed at attach, close and exit. Written synchronously, one line per\n// act: acts are rare and a partial line on a crash is worse than a syscall.\n// Nothing here throws into the agent: the first failure latches the log off,\n// reports once through `onFailure`, and every later append is a no-op.\n// `tail` reads the log back for the harness.\nimport { closeSync, fstatSync, mkdirSync, openSync, readSync, writeSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { cutUtf8, type DecisionDecl } from \"./manifest/manifest.ts\";\n\nexport type AuditAct = \"capability\" | \"knob\" | \"decision\" | \"turn\" | \"lifecycle\" | \"counters\";\nexport type AuditOutcome = \"ok\" | \"error\" | \"forbidden\" | \"agent\" | \"default\" | \"stub\" | \"failed\";\n\nexport interface AuditRecord {\n\tts: string;\n\tpid: number;\n\t/** The connection that acted (`k1`, `k2`, ...); absent for counters. */\n\tconn?: string;\n\tact: AuditAct;\n\t/** The capability, cell, decision or counter name. */\n\tname: string;\n\t/** The request id or hook id. */\n\tid?: string;\n\t/** The parameters (capped, see `capValue`). */\n\tparams?: unknown;\n\t/** A knob's landed value, or a decision answer's summary. */\n\tvalue?: unknown;\n\toutcome: AuditOutcome;\n\t/** Why a default or stub answer applied, or what a counter counts. */\n\tcause?: string;\n\t/** What the harness stated for an act whose policy is `confirm`: approved, auto, or denied. */\n\tconsent?: string;\n\t/** The agent's stated reason. */\n\treason?: unknown;\n\terror?: unknown;\n\t/** Milliseconds from dispatch to settle. */\n\tms?: number;\n\t/** Counters only. */\n\tcount?: number;\n}\n\n/** Bytes a string or the JSON of a value may take in a record before it is cut. */\nexport const AUDIT_VALUE_CAP = 512;\n/** Bytes `tail` reads from the end of the file. */\nexport const AUDIT_TAIL_BYTES = 1024 * 1024;\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/**\n * Cap a value for a record: a value whose JSON exceeds the cap becomes\n * `{truncated: true, bytes, head}` with the JSON's first 512 characters, so\n * the record stays valid JSON. A value that cannot be serialized becomes a\n * marker string.\n */\nexport function capValue(value: unknown): unknown {\n\tif (value === undefined) return undefined;\n\tlet json: string;\n\ttry {\n\t\tjson = JSON.stringify(value) ?? \"null\";\n\t} catch (error) {\n\t\treturn `<unserializable: ${errorMessage(error)}>`;\n\t}\n\tconst bytes = Buffer.byteLength(json, \"utf8\");\n\tif (bytes <= AUDIT_VALUE_CAP) return value;\n\treturn { truncated: true, bytes, head: cutUtf8(json, AUDIT_VALUE_CAP) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * What an audit record keeps of a decision answer: never the transcript.\n * An options decision keeps the chosen option; any other answer keeps\n * whether it changed something (a non-empty object).\n */\nexport function summarizeAnswer(decision: DecisionDecl | undefined, answer: unknown): unknown {\n\tif (answer === undefined) return undefined;\n\tif (decision?.options && typeof answer === \"string\") return { option: answer };\n\treturn { changed: Object.keys(isRecord(answer) ? answer : {}).length > 0 };\n}\n\nexport interface AuditTail {\n\trecords: unknown[];\n\t/** More records exist than were returned. */\n\ttruncated: boolean;\n}\n\nexport class AuditLog {\n\treadonly path: string;\n\tprivate readonly onFailure: ((error: string) => void) | undefined;\n\tprivate fd: number | undefined;\n\tprivate _failed: string | undefined;\n\tprivate closed = false;\n\n\tconstructor(path: string, onFailure?: (error: string) => void) {\n\t\tthis.path = path;\n\t\tthis.onFailure = onFailure;\n\t}\n\n\t/** The error that latched the log off, if any. */\n\tget failed(): string | undefined {\n\t\treturn this._failed;\n\t}\n\n\t/** Append one record. Never throws; a no-op once the log failed or was closed. */\n\tappend(record: Omit<AuditRecord, \"ts\" | \"pid\">): void {\n\t\tif (this._failed !== undefined || this.closed) return;\n\t\ttry {\n\t\t\tif (this.fd === undefined) {\n\t\t\t\tmkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });\n\t\t\t\tthis.fd = openSync(this.path, \"a\", 0o600);\n\t\t\t}\n\t\t\tconst full: AuditRecord = { ts: new Date().toISOString(), pid: process.pid, ...record };\n\t\t\twriteSync(this.fd, `${JSON.stringify(full)}\\n`);\n\t\t} catch (error) {\n\t\t\tthis.fail(errorMessage(error));\n\t\t}\n\t}\n\n\tprivate fail(error: string): void {\n\t\tthis._failed = error;\n\t\tthis.close();\n\t\ttry {\n\t\t\tthis.onFailure?.(error);\n\t\t} catch {\n\t\t\t// a failure callback must not fail the caller\n\t\t}\n\t}\n\n\t/** Close the file descriptor; later appends are dropped. Idempotent; safe inside `process.on(\"exit\")`. */\n\tclose(): void {\n\t\tthis.closed = true;\n\t\tif (this.fd === undefined) return;\n\t\tconst fd = this.fd;\n\t\tthis.fd = undefined;\n\t\ttry {\n\t\t\tcloseSync(fd);\n\t\t} catch {\n\t\t\t// ignore\n\t\t}\n\t}\n\n\t/** The last `limit` records, read from the last megabyte of the file. Never throws. */\n\ttail(limit: number): AuditTail {\n\t\tlet fd: number | undefined;\n\t\ttry {\n\t\t\tfd = openSync(this.path, \"r\");\n\t\t\tconst size = fstatSync(fd).size;\n\t\t\tconst start = Math.max(0, size - AUDIT_TAIL_BYTES);\n\t\t\tconst buffer = Buffer.alloc(size - start);\n\t\t\tlet read = 0;\n\t\t\twhile (read < buffer.length) {\n\t\t\t\tconst n = readSync(fd, buffer, read, buffer.length - read, start + read);\n\t\t\t\tif (n === 0) break;\n\t\t\t\tread += n;\n\t\t\t}\n\t\t\tconst lines = buffer.toString(\"utf8\", 0, read).split(\"\\n\");\n\t\t\t// the window may start mid-line: drop the first line unless the byte before it ends a line\n\t\t\tif (start > 0) {\n\t\t\t\tconst before = Buffer.alloc(1);\n\t\t\t\tconst n = readSync(fd, before, 0, 1, start - 1);\n\t\t\t\tif (n !== 1 || before[0] !== 0x0a) lines.shift();\n\t\t\t}\n\t\t\tconst records: unknown[] = [];\n\t\t\tfor (const line of lines) {\n\t\t\t\tif (line === \"\") continue;\n\t\t\t\ttry {\n\t\t\t\t\trecords.push(JSON.parse(line));\n\t\t\t\t} catch {\n\t\t\t\t\trecords.push({ raw: line });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst truncated = start > 0 || records.length > limit;\n\t\t\treturn { records: records.slice(-limit), truncated };\n\t\t} catch (error) {\n\t\t\t// no log yet, or a path that can never hold one (the failure is in `failed`)\n\t\t\tconst code = (error as NodeJS.ErrnoException).code;\n\t\t\tif (code === \"ENOENT\" || code === \"ENOTDIR\") return { records: [], truncated: false };\n\t\t\treturn { records: [{ error: errorMessage(error) }], truncated: false };\n\t\t} finally {\n\t\t\tif (fd !== undefined) {\n\t\t\t\ttry {\n\t\t\t\t\tcloseSync(fd);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CellDecl } from "../manifest/manifest.ts";
|
|
2
|
+
/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */
|
|
3
|
+
export declare function fitValue(value: unknown, decl: CellDecl): unknown;
|
|
4
|
+
/** Whether a value can be written to a knob: the declared type, enum and range. */
|
|
5
|
+
export declare function validateKnobValue(decl: CellDecl, value: unknown): string | undefined;
|
|
6
|
+
//# sourceMappingURL=fit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fit.d.ts","sourceRoot":"","sources":["../../src/cells/fit.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,QAAQ,EAAiE,MAAM,yBAAyB,CAAC;AAOvH,mGAAmG;AACnG,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAsBhE;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQpF","sourcesContent":["// Fitting cell values to their declared sizes and validating knob writes\n// (abaigrammar section 5.2).\nimport { type CellDecl, cutUtf8, encodedBytes, type FieldSchema, fieldOf, matchesType } from \"../manifest/manifest.ts\";\n\n/** Cut a string to at most `max` bytes without splitting a code point (one encode, one decode). */\nfunction cutString(value: string, max: number): string {\n\treturn cutUtf8(value, max);\n}\n\n/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */\nexport function fitValue(value: unknown, decl: CellDecl): unknown {\n\tif (decl.type === \"string\" && typeof value === \"string\") {\n\t\treturn cutString(value, decl.max ?? 256);\n\t}\n\tif (decl.type === \"object\" && typeof value === \"object\" && value !== null) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst bytes = encodedBytes(value);\n\t\tif (bytes > max) return { truncated: true, bytes };\n\t\treturn JSON.parse(JSON.stringify(value));\n\t}\n\tif (decl.type === \"list\" && Array.isArray(value)) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst items = decl.items === undefined ? undefined : fieldOf(decl.items);\n\t\tconst kept: unknown[] = [];\n\t\tfor (const item of value) {\n\t\t\tconst fitted = items?.type === \"string\" && typeof item === \"string\" ? cutString(item, items.max ?? 256) : item;\n\t\t\tif (encodedBytes([...kept, fitted]) > max) break;\n\t\t\tkept.push(fitted);\n\t\t}\n\t\treturn kept;\n\t}\n\treturn value;\n}\n\n/** Whether a value can be written to a knob: the declared type, enum and range. */\nexport function validateKnobValue(decl: CellDecl, value: unknown): string | undefined {\n\tif (!matchesType(value, decl as unknown as FieldSchema)) {\n\t\treturn decl.type === \"enum\" ? `value must be one of: ${decl.enum?.join(\", \")}` : `value must be a ${decl.type}`;\n\t}\n\tif (decl.range && typeof value === \"number\" && (value < decl.range[0] || value > decl.range[1])) {\n\t\treturn `value must be within ${decl.range[0]} and ${decl.range[1]}`;\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Fitting cell values to their declared sizes and validating knob writes
|
|
2
|
+
// (abaigrammar section 5.2).
|
|
3
|
+
import { cutUtf8, encodedBytes, fieldOf, matchesType } from "../manifest/manifest.js";
|
|
4
|
+
/** Cut a string to at most `max` bytes without splitting a code point (one encode, one decode). */
|
|
5
|
+
function cutString(value, max) {
|
|
6
|
+
return cutUtf8(value, max);
|
|
7
|
+
}
|
|
8
|
+
/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */
|
|
9
|
+
export function fitValue(value, decl) {
|
|
10
|
+
if (decl.type === "string" && typeof value === "string") {
|
|
11
|
+
return cutString(value, decl.max ?? 256);
|
|
12
|
+
}
|
|
13
|
+
if (decl.type === "object" && typeof value === "object" && value !== null) {
|
|
14
|
+
const max = decl.max ?? 1024;
|
|
15
|
+
const bytes = encodedBytes(value);
|
|
16
|
+
if (bytes > max)
|
|
17
|
+
return { truncated: true, bytes };
|
|
18
|
+
return JSON.parse(JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
if (decl.type === "list" && Array.isArray(value)) {
|
|
21
|
+
const max = decl.max ?? 1024;
|
|
22
|
+
const items = decl.items === undefined ? undefined : fieldOf(decl.items);
|
|
23
|
+
const kept = [];
|
|
24
|
+
for (const item of value) {
|
|
25
|
+
const fitted = items?.type === "string" && typeof item === "string" ? cutString(item, items.max ?? 256) : item;
|
|
26
|
+
if (encodedBytes([...kept, fitted]) > max)
|
|
27
|
+
break;
|
|
28
|
+
kept.push(fitted);
|
|
29
|
+
}
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
/** Whether a value can be written to a knob: the declared type, enum and range. */
|
|
35
|
+
export function validateKnobValue(decl, value) {
|
|
36
|
+
if (!matchesType(value, decl)) {
|
|
37
|
+
return decl.type === "enum" ? `value must be one of: ${decl.enum?.join(", ")}` : `value must be a ${decl.type}`;
|
|
38
|
+
}
|
|
39
|
+
if (decl.range && typeof value === "number" && (value < decl.range[0] || value > decl.range[1])) {
|
|
40
|
+
return `value must be within ${decl.range[0]} and ${decl.range[1]}`;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=fit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fit.js","sourceRoot":"","sources":["../../src/cells/fit.ts"],"names":[],"mappings":"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","sourcesContent":["// Fitting cell values to their declared sizes and validating knob writes\n// (abaigrammar section 5.2).\nimport { type CellDecl, cutUtf8, encodedBytes, type FieldSchema, fieldOf, matchesType } from \"../manifest/manifest.ts\";\n\n/** Cut a string to at most `max` bytes without splitting a code point (one encode, one decode). */\nfunction cutString(value: string, max: number): string {\n\treturn cutUtf8(value, max);\n}\n\n/** Fit a value to its cell: strings cut to `max` bytes, lists cut to the items whose JSON fits. */\nexport function fitValue(value: unknown, decl: CellDecl): unknown {\n\tif (decl.type === \"string\" && typeof value === \"string\") {\n\t\treturn cutString(value, decl.max ?? 256);\n\t}\n\tif (decl.type === \"object\" && typeof value === \"object\" && value !== null) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst bytes = encodedBytes(value);\n\t\tif (bytes > max) return { truncated: true, bytes };\n\t\treturn JSON.parse(JSON.stringify(value));\n\t}\n\tif (decl.type === \"list\" && Array.isArray(value)) {\n\t\tconst max = decl.max ?? 1024;\n\t\tconst items = decl.items === undefined ? undefined : fieldOf(decl.items);\n\t\tconst kept: unknown[] = [];\n\t\tfor (const item of value) {\n\t\t\tconst fitted = items?.type === \"string\" && typeof item === \"string\" ? cutString(item, items.max ?? 256) : item;\n\t\t\tif (encodedBytes([...kept, fitted]) > max) break;\n\t\t\tkept.push(fitted);\n\t\t}\n\t\treturn kept;\n\t}\n\treturn value;\n}\n\n/** Whether a value can be written to a knob: the declared type, enum and range. */\nexport function validateKnobValue(decl: CellDecl, value: unknown): string | undefined {\n\tif (!matchesType(value, decl as unknown as FieldSchema)) {\n\t\treturn decl.type === \"enum\" ? `value must be one of: ${decl.enum?.join(\", \")}` : `value must be a ${decl.type}`;\n\t}\n\tif (decl.range && typeof value === \"number\" && (value < decl.range[0] || value > decl.range[1])) {\n\t\treturn `value must be within ${decl.range[0]} and ${decl.range[1]}`;\n\t}\n\treturn undefined;\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Manifest } from "./manifest/manifest.ts";
|
|
2
|
+
export declare const MANIFEST_FILE = "abaigram.yaml";
|
|
3
|
+
export interface ContentSlots {
|
|
4
|
+
readonly root: string;
|
|
5
|
+
/** The identity text; throws when the slot is missing or empty. */
|
|
6
|
+
identity(): string;
|
|
7
|
+
install(): string | undefined;
|
|
8
|
+
use(): string | undefined;
|
|
9
|
+
/** The content for a named turn or decision, from the `turns` directory. */
|
|
10
|
+
turn(name: string): string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Whether the manifest file at `path` names this program. */
|
|
13
|
+
export declare function isManifestOf(path: string, name: string): boolean;
|
|
14
|
+
/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */
|
|
15
|
+
export declare function findRoot(name: string, dir?: string): string | undefined;
|
|
16
|
+
export declare function createContent(manifest: Manifest, resolveRoot: () => string): ContentSlots;
|
|
17
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,IAAI,MAAM,GAAG,SAAS,CAAC;IAC9B,GAAG,IAAI,MAAM,GAAG,SAAS,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOhE;AAED,4FAA4F;AAC5F,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,SAAS,CAQtF;AAOD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,MAAM,GAAG,YAAY,CA8BzF","sourcesContent":["// The content slots (abaigrammar section 8): the abaigrammer's prompts, read\n// from files under the program's root. The root is where abaigram.yaml lives:\n// an explicit option, else the nearest ancestor of the cwd holding this\n// program's manifest, else the cwd. Reads are lazy, so a program that never\n// attaches never touches the files.\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { Manifest } from \"./manifest/manifest.ts\";\nimport { parseManifestYaml } from \"./manifest/render.ts\";\n\nexport const MANIFEST_FILE = \"abaigram.yaml\";\n\nexport interface ContentSlots {\n\treadonly root: string;\n\t/** The identity text; throws when the slot is missing or empty. */\n\tidentity(): string;\n\tinstall(): string | undefined;\n\tuse(): string | undefined;\n\t/** The content for a named turn or decision, from the `turns` directory. */\n\tturn(name: string): string | undefined;\n}\n\n/** Whether the manifest file at `path` names this program. */\nexport function isManifestOf(path: string, name: string): boolean {\n\ttry {\n\t\tconst parsed = parseManifestYaml(readFileSync(path, \"utf8\"));\n\t\treturn typeof parsed === \"object\" && parsed !== null && (parsed as { name?: unknown }).name === name;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */\nexport function findRoot(name: string, dir: string = process.cwd()): string | undefined {\n\tlet current = resolve(dir);\n\twhile (true) {\n\t\tif (isManifestOf(join(current, MANIFEST_FILE), name)) return current;\n\t\tconst parent = dirname(current);\n\t\tif (parent === current) return undefined;\n\t\tcurrent = parent;\n\t}\n}\n\nfunction readOptional(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\treturn readFileSync(path, \"utf8\");\n}\n\nexport function createContent(manifest: Manifest, resolveRoot: () => string): ContentSlots {\n\tlet found: string | undefined;\n\tconst root = (): string => {\n\t\tif (found === undefined) found = resolveRoot();\n\t\treturn found;\n\t};\n\tconst slot = (relative: string): string => resolve(root(), relative);\n\treturn {\n\t\tget root() {\n\t\t\treturn root();\n\t\t},\n\t\tidentity() {\n\t\t\tconst path = slot(manifest.content.identity);\n\t\t\tconst text = readOptional(path);\n\t\t\tif (text === undefined) throw new Error(`the identity content is missing: ${path}`);\n\t\t\tif (text.trim() === \"\") throw new Error(`the identity content is empty: ${path}`);\n\t\t\treturn text;\n\t\t},\n\t\t// the undeclared slots default to the abai/ directory (section 8); a missing file is simply no content\n\t\tinstall() {\n\t\t\treturn readOptional(slot(manifest.content.install ?? \"abai/install.md\"));\n\t\t},\n\t\tuse() {\n\t\t\treturn readOptional(slot(manifest.content.use ?? \"abai/use.md\"));\n\t\t},\n\t\tturn(name) {\n\t\t\tconst dir = manifest.content.turns ?? \"abai/turns\";\n\t\t\treturn readOptional(slot(join(dir, `${name}.md`)));\n\t\t},\n\t};\n}\n"]}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The content slots (abaigrammar section 8): the abaigrammer's prompts, read
|
|
2
|
+
// from files under the program's root. The root is where abaigram.yaml lives:
|
|
3
|
+
// an explicit option, else the nearest ancestor of the cwd holding this
|
|
4
|
+
// program's manifest, else the cwd. Reads are lazy, so a program that never
|
|
5
|
+
// attaches never touches the files.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { parseManifestYaml } from "./manifest/render.js";
|
|
9
|
+
export const MANIFEST_FILE = "abaigram.yaml";
|
|
10
|
+
/** Whether the manifest file at `path` names this program. */
|
|
11
|
+
export function isManifestOf(path, name) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = parseManifestYaml(readFileSync(path, "utf8"));
|
|
14
|
+
return typeof parsed === "object" && parsed !== null && parsed.name === name;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */
|
|
21
|
+
export function findRoot(name, dir = process.cwd()) {
|
|
22
|
+
let current = resolve(dir);
|
|
23
|
+
while (true) {
|
|
24
|
+
if (isManifestOf(join(current, MANIFEST_FILE), name))
|
|
25
|
+
return current;
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current)
|
|
28
|
+
return undefined;
|
|
29
|
+
current = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readOptional(path) {
|
|
33
|
+
if (!existsSync(path))
|
|
34
|
+
return undefined;
|
|
35
|
+
return readFileSync(path, "utf8");
|
|
36
|
+
}
|
|
37
|
+
export function createContent(manifest, resolveRoot) {
|
|
38
|
+
let found;
|
|
39
|
+
const root = () => {
|
|
40
|
+
if (found === undefined)
|
|
41
|
+
found = resolveRoot();
|
|
42
|
+
return found;
|
|
43
|
+
};
|
|
44
|
+
const slot = (relative) => resolve(root(), relative);
|
|
45
|
+
return {
|
|
46
|
+
get root() {
|
|
47
|
+
return root();
|
|
48
|
+
},
|
|
49
|
+
identity() {
|
|
50
|
+
const path = slot(manifest.content.identity);
|
|
51
|
+
const text = readOptional(path);
|
|
52
|
+
if (text === undefined)
|
|
53
|
+
throw new Error(`the identity content is missing: ${path}`);
|
|
54
|
+
if (text.trim() === "")
|
|
55
|
+
throw new Error(`the identity content is empty: ${path}`);
|
|
56
|
+
return text;
|
|
57
|
+
},
|
|
58
|
+
// the undeclared slots default to the abai/ directory (section 8); a missing file is simply no content
|
|
59
|
+
install() {
|
|
60
|
+
return readOptional(slot(manifest.content.install ?? "abai/install.md"));
|
|
61
|
+
},
|
|
62
|
+
use() {
|
|
63
|
+
return readOptional(slot(manifest.content.use ?? "abai/use.md"));
|
|
64
|
+
},
|
|
65
|
+
turn(name) {
|
|
66
|
+
const dir = manifest.content.turns ?? "abai/turns";
|
|
67
|
+
return readOptional(slot(join(dir, `${name}.md`)));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"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","sourcesContent":["// The content slots (abaigrammar section 8): the abaigrammer's prompts, read\n// from files under the program's root. The root is where abaigram.yaml lives:\n// an explicit option, else the nearest ancestor of the cwd holding this\n// program's manifest, else the cwd. Reads are lazy, so a program that never\n// attaches never touches the files.\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport type { Manifest } from \"./manifest/manifest.ts\";\nimport { parseManifestYaml } from \"./manifest/render.ts\";\n\nexport const MANIFEST_FILE = \"abaigram.yaml\";\n\nexport interface ContentSlots {\n\treadonly root: string;\n\t/** The identity text; throws when the slot is missing or empty. */\n\tidentity(): string;\n\tinstall(): string | undefined;\n\tuse(): string | undefined;\n\t/** The content for a named turn or decision, from the `turns` directory. */\n\tturn(name: string): string | undefined;\n}\n\n/** Whether the manifest file at `path` names this program. */\nexport function isManifestOf(path: string, name: string): boolean {\n\ttry {\n\t\tconst parsed = parseManifestYaml(readFileSync(path, \"utf8\"));\n\t\treturn typeof parsed === \"object\" && parsed !== null && (parsed as { name?: unknown }).name === name;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/** The nearest ancestor of `dir` (itself included) holding this program's abaigram.yaml. */\nexport function findRoot(name: string, dir: string = process.cwd()): string | undefined {\n\tlet current = resolve(dir);\n\twhile (true) {\n\t\tif (isManifestOf(join(current, MANIFEST_FILE), name)) return current;\n\t\tconst parent = dirname(current);\n\t\tif (parent === current) return undefined;\n\t\tcurrent = parent;\n\t}\n}\n\nfunction readOptional(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\treturn readFileSync(path, \"utf8\");\n}\n\nexport function createContent(manifest: Manifest, resolveRoot: () => string): ContentSlots {\n\tlet found: string | undefined;\n\tconst root = (): string => {\n\t\tif (found === undefined) found = resolveRoot();\n\t\treturn found;\n\t};\n\tconst slot = (relative: string): string => resolve(root(), relative);\n\treturn {\n\t\tget root() {\n\t\t\treturn root();\n\t\t},\n\t\tidentity() {\n\t\t\tconst path = slot(manifest.content.identity);\n\t\t\tconst text = readOptional(path);\n\t\t\tif (text === undefined) throw new Error(`the identity content is missing: ${path}`);\n\t\t\tif (text.trim() === \"\") throw new Error(`the identity content is empty: ${path}`);\n\t\t\treturn text;\n\t\t},\n\t\t// the undeclared slots default to the abai/ directory (section 8); a missing file is simply no content\n\t\tinstall() {\n\t\t\treturn readOptional(slot(manifest.content.install ?? \"abai/install.md\"));\n\t\t},\n\t\tuse() {\n\t\t\treturn readOptional(slot(manifest.content.use ?? \"abai/use.md\"));\n\t\t},\n\t\tturn(name) {\n\t\t\tconst dir = manifest.content.turns ?? \"abai/turns\";\n\t\t\treturn readOptional(slot(join(dir, `${name}.md`)));\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Abaigram } from "./program.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Where the socket goes when the run directory's own path is too long for
|
|
4
|
+
* one (103 bytes on macOS, and a run directory under a deep project path
|
|
5
|
+
* gets there): a private directory under the machine's temp dir. The
|
|
6
|
+
* descriptor in the run directory names the socket wherever it is, which is
|
|
7
|
+
* what its `socket` field is for, so discovery is unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shortSocketPath(pid: number): string;
|
|
10
|
+
export interface EndpointOptions {
|
|
11
|
+
/** Where the socket and the descriptor go. */
|
|
12
|
+
runDir: string;
|
|
13
|
+
/** The socket path; default `<runDir>/<pid>.sock`. */
|
|
14
|
+
socketPath?: string;
|
|
15
|
+
/** A connection whose unsent output exceeds this many bytes is dropped whole. Default 16 MiB. */
|
|
16
|
+
maxBuffered?: number;
|
|
17
|
+
/** An input line past this many bytes drops the connection. Default 64 MiB. */
|
|
18
|
+
maxLineBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
/** Unsent bytes a connection may hold before it is dropped: a harness that cannot keep up is not the program's problem. */
|
|
21
|
+
export declare const DEFAULT_MAX_BUFFERED: number;
|
|
22
|
+
/** Consecutive claim timeouts after which a connection's claims are released: a harness that never answers holds nothing. */
|
|
23
|
+
export declare const CONSECUTIVE_TIMEOUTS_LIMIT = 3;
|
|
24
|
+
export declare class Endpoint {
|
|
25
|
+
readonly runDir: string;
|
|
26
|
+
readonly socketPath: string;
|
|
27
|
+
/** Resolves once the socket listens and the descriptor is written; rejects with why it could not. */
|
|
28
|
+
readonly ready: Promise<void>;
|
|
29
|
+
private readonly app;
|
|
30
|
+
private readonly maxBuffered;
|
|
31
|
+
private readonly maxLineBytes;
|
|
32
|
+
private server;
|
|
33
|
+
private readonly connections;
|
|
34
|
+
private readonly claimants;
|
|
35
|
+
private readonly hooks;
|
|
36
|
+
/** Open turns and touchpoints, replayed to a connection that arrives while they are open. */
|
|
37
|
+
private readonly openTurns;
|
|
38
|
+
private readonly openTouchpoints;
|
|
39
|
+
private readonly connectionWaiters;
|
|
40
|
+
private binding;
|
|
41
|
+
private unsubscribe;
|
|
42
|
+
private nextConnectionId;
|
|
43
|
+
private closed;
|
|
44
|
+
private readonly onExit;
|
|
45
|
+
private settleReady;
|
|
46
|
+
constructor(app: Abaigram, options: EndpointOptions);
|
|
47
|
+
private resolveReady;
|
|
48
|
+
private rejectReady;
|
|
49
|
+
get connectionCount(): number;
|
|
50
|
+
/** The ids of the open connections, in order of arrival. */
|
|
51
|
+
connectionIds(): string[];
|
|
52
|
+
private listen;
|
|
53
|
+
/** Resolves true as soon as a connection is open, or when one arrives within `timeoutMs`; false when none does. */
|
|
54
|
+
waitForConnection(timeoutMs: number): Promise<boolean>;
|
|
55
|
+
/** Stop listening, drop every connection, remove the socket and the descriptor. */
|
|
56
|
+
close(reason?: string): void;
|
|
57
|
+
private removeFiles;
|
|
58
|
+
private accept;
|
|
59
|
+
/** The one binding every connection shares, created by the first. Its own label is for acts no connection made. */
|
|
60
|
+
private bindingFor;
|
|
61
|
+
private hello;
|
|
62
|
+
private write;
|
|
63
|
+
private drop;
|
|
64
|
+
/** Forget the binding and everything pending on it, every timer included: a finished program must be free to exit. */
|
|
65
|
+
private releaseBinding;
|
|
66
|
+
/** The program detached the binding itself: whoever is still connected is told by the close. */
|
|
67
|
+
private onDetach;
|
|
68
|
+
private onLine;
|
|
69
|
+
private respond;
|
|
70
|
+
private failure;
|
|
71
|
+
private dispatch;
|
|
72
|
+
private subscribe;
|
|
73
|
+
private accepts;
|
|
74
|
+
private broadcastEvent;
|
|
75
|
+
private claim;
|
|
76
|
+
private release;
|
|
77
|
+
/** Free some of a connection's claims; what it was asked under them takes the default now. */
|
|
78
|
+
private releaseClaims;
|
|
79
|
+
private forget;
|
|
80
|
+
/** The program asked a decision: it goes to the claimant, or takes its default now. */
|
|
81
|
+
private onDecision;
|
|
82
|
+
/** The binding settled a decision by timeout (its deadline, or the claim's wait through `defaultDecision`). */
|
|
83
|
+
private onDecisionTimeout;
|
|
84
|
+
private onHookResponse;
|
|
85
|
+
private turnMessage;
|
|
86
|
+
private touchpointMessage;
|
|
87
|
+
private onTurn;
|
|
88
|
+
private onTouchpoint;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=endpoint.d.ts.map
|