@aiwg/cli 2026.8.25 → 2026.8.26
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/bin/aiwg.mjs +25 -16
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/features/catalog.js +11 -0
- package/dist/src/storage/backend-contract.js +11 -2
- package/dist/src/storage/backends/postgres-schema.js +144 -0
- package/dist/src/storage/backends/postgres.js +468 -0
- package/dist/src/storage/backends/postgrest-schema.js +192 -0
- package/dist/src/storage/backends/postgrest.js +346 -0
- package/dist/src/storage/index.js +5 -0
- package/dist/src/storage/qualification.js +137 -0
- package/dist/src/update/notifier.mjs +59 -14
- package/package.json +1 -1
package/bin/aiwg.mjs
CHANGED
|
@@ -203,13 +203,6 @@ function maybeHandleFastHelp(args) {
|
|
|
203
203
|
return true;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
if (maybeHandleFastVersion(process.argv.slice(2))) {
|
|
207
|
-
process.exit(0);
|
|
208
|
-
}
|
|
209
|
-
if (maybeHandleFastHelp(process.argv.slice(2))) {
|
|
210
|
-
process.exit(0);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
206
|
// Preflight: verify dist/ is built before any of the dynamic imports below
|
|
214
207
|
// try to resolve files that don't exist. Without this, a missing/incomplete
|
|
215
208
|
// dist/ surfaces as a raw `Cannot find module '.../dist/src/.../*.mjs'` from
|
|
@@ -240,6 +233,31 @@ function maybeWarnUnbuiltDist() {
|
|
|
240
233
|
}
|
|
241
234
|
maybeWarnUnbuiltDist();
|
|
242
235
|
|
|
236
|
+
// Display a cached notice and schedule its refresh before every eligible CLI
|
|
237
|
+
// path, including fast help/version, channel recovery, and later preflight
|
|
238
|
+
// failures. This local-only bootstrap never waits on the registry and failures
|
|
239
|
+
// are deliberately ignored so update advice cannot change command behavior.
|
|
240
|
+
async function runUpdateNotifierBootstrap() {
|
|
241
|
+
try {
|
|
242
|
+
const notifierPath = path.join(packageRoot, 'dist', 'src', 'update', 'notifier.mjs');
|
|
243
|
+
const notifier = await import(pathToFileURL(notifierPath).href);
|
|
244
|
+
const activePackageRoot = notifier.resolveActivePackageRoot(packageRoot);
|
|
245
|
+
notifier.maybePrintNotice(activePackageRoot);
|
|
246
|
+
notifier.scheduleBackgroundCheck(activePackageRoot);
|
|
247
|
+
} catch {
|
|
248
|
+
// Best effort: installation/router diagnostics retain authority.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await runUpdateNotifierBootstrap();
|
|
253
|
+
|
|
254
|
+
if (maybeHandleFastVersion(process.argv.slice(2))) {
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
if (maybeHandleFastHelp(process.argv.slice(2))) {
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
243
261
|
// Mint or inherit an invocation ID before anything else loads. Child processes
|
|
244
262
|
// spawned by handlers (detached update-notifier, aiwg exec, etc.) inherit the
|
|
245
263
|
// parent's ID via `AIWG_INVOCATION_ID` so their JSONL log records correlate
|
|
@@ -398,15 +416,6 @@ async function main() {
|
|
|
398
416
|
// tag every record with it.
|
|
399
417
|
await applyVerbosityFromArgs(args, routerPath);
|
|
400
418
|
|
|
401
|
-
// Update notifier: print any pending notice from the previous run's
|
|
402
|
-
// background check, then schedule the next background check. Both are
|
|
403
|
-
// non-blocking — the current command never waits on the network.
|
|
404
|
-
// Honors NO_UPDATE_NOTIFIER, CI=*, and non-TTY stderr.
|
|
405
|
-
const notifierPath = path.join(activePackageRoot, 'dist', 'src', 'update', 'notifier.mjs');
|
|
406
|
-
const { scheduleBackgroundCheck, maybePrintNotice } = await import(pathToFileURL(notifierPath).href);
|
|
407
|
-
maybePrintNotice(activePackageRoot);
|
|
408
|
-
scheduleBackgroundCheck(activePackageRoot);
|
|
409
|
-
|
|
410
419
|
// Top-level cancellation controller. SIGINT / SIGTERM flip it, long-running
|
|
411
420
|
// handlers plumb ctx.signal through fetches and loops so Ctrl-C cancels
|
|
412
421
|
// in-flight work cleanly instead of leaving orphaned sockets and children.
|
package/dist/src/api/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { run } from '../cli/router.js';
|
|
|
9
9
|
export * from '../resources/index.js';
|
|
10
10
|
export * from '../sessions/index.js';
|
|
11
11
|
export * from '../memory/index.js';
|
|
12
|
+
export * from '../storage/index.js';
|
|
12
13
|
export * from '../security/threat-assessment-config.js';
|
|
13
14
|
export * from '../security/artifact-verifier.js';
|
|
14
15
|
export * from '../security/artifact-attestation.js';
|
package/dist/src/api/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { run } from '../cli/router.js';
|
|
|
9
9
|
export * from '../resources/index.js';
|
|
10
10
|
export * from '../sessions/index.js';
|
|
11
11
|
export * from '../memory/index.js';
|
|
12
|
+
export * from '../storage/index.js';
|
|
12
13
|
export * from '../security/threat-assessment-config.js';
|
|
13
14
|
export * from '../security/artifact-verifier.js';
|
|
14
15
|
export * from '../security/artifact-attestation.js';
|
|
@@ -42,6 +42,17 @@ export const FEATURE_CATALOG = [
|
|
|
42
42
|
],
|
|
43
43
|
cost: '~5 MB — native compile via node-gyp',
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
name: 'postgres',
|
|
47
|
+
description: 'Advanced direct PostgreSQL canonical-storage backend',
|
|
48
|
+
packages: ['pg'],
|
|
49
|
+
packageSpecs: { pg: '8.23.0' },
|
|
50
|
+
enables: [
|
|
51
|
+
'aiwg.storage-backend/v1 direct PostgreSQL persistence',
|
|
52
|
+
'transactional migration batches, snapshots, cursors, and tombstones',
|
|
53
|
+
],
|
|
54
|
+
cost: '~1 MB — pure JavaScript PostgreSQL client',
|
|
55
|
+
},
|
|
45
56
|
{
|
|
46
57
|
name: 'pty',
|
|
47
58
|
description: 'PTY bridge for daemon — pass-through interactive TUIs (Claude Code, Codex, etc.)',
|
|
@@ -43,8 +43,17 @@ export const STORAGE_BACKEND_MATRIX = {
|
|
|
43
43
|
sqlite: descriptor('sqlite', 'supported', [...BASELINE, 'atomic-batch', 'consistent-snapshot', 'tombstones', 'idempotency-keys', 'filtered-query', 'cursor-pagination', 'backup', 'restore'], 'wal', 'single-host', 'serializable', 'regenerable-index'),
|
|
44
44
|
'fortemi-core-static': descriptor('fortemi-core-static', 'supported', ['read', 'filtered-query', 'recursive-traversal', 'set-operations'], 'filesystem', 'single-host', 'snapshot', 'static-cache'),
|
|
45
45
|
'fortemi-server': descriptor('fortemi-server', 'alpha', [...BASELINE, 'filtered-query', 'health', 'tls', 'tenant-isolation'], 'replicated', 'remote-service', 'none', 'remote-persistence'),
|
|
46
|
-
'postgres-direct': descriptor('postgres-direct', 'advanced', [
|
|
47
|
-
|
|
46
|
+
'postgres-direct': descriptor('postgres-direct', 'advanced', [
|
|
47
|
+
...BASELINE, 'atomic-batch', 'consistent-snapshot', 'change-cursor',
|
|
48
|
+
'tombstones', 'idempotency-keys', 'recursive-traversal', 'set-operations',
|
|
49
|
+
'filtered-query', 'cursor-pagination', 'health', 'readiness', 'telemetry',
|
|
50
|
+
'tenant-isolation', 'tls',
|
|
51
|
+
], 'replicated', 'remote-service', 'serializable', 'canonical'),
|
|
52
|
+
'postgres-postgrest': descriptor('postgres-postgrest', 'advanced', [
|
|
53
|
+
...BASELINE, 'atomic-batch', 'consistent-snapshot', 'change-cursor',
|
|
54
|
+
'tombstones', 'idempotency-keys', 'filtered-query', 'cursor-pagination',
|
|
55
|
+
'health', 'readiness', 'tenant-isolation', 'tls',
|
|
56
|
+
], 'replicated', 'remote-service', 'serializable', 'canonical'),
|
|
48
57
|
mysql: descriptor('mysql', 'deferred', [], 'replicated', 'remote-service', 'none', 'canonical'),
|
|
49
58
|
};
|
|
50
59
|
function descriptor(backend, maturity, capabilities, durability, availability, isolation, dataClass) {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export const POSTGRES_SCHEMA_VERSION = 1;
|
|
2
|
+
export const POSTGRES_SCHEMA_V1_SQL = `
|
|
3
|
+
CREATE TABLE IF NOT EXISTS aiwg_storage_schema (
|
|
4
|
+
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
|
5
|
+
schema_version integer NOT NULL
|
|
6
|
+
);
|
|
7
|
+
INSERT INTO aiwg_storage_schema(singleton, schema_version)
|
|
8
|
+
VALUES (true, 1) ON CONFLICT (singleton) DO NOTHING;
|
|
9
|
+
CREATE TABLE IF NOT EXISTS aiwg_storage_records (
|
|
10
|
+
tenant text NOT NULL,
|
|
11
|
+
subsystem text NOT NULL,
|
|
12
|
+
path text NOT NULL,
|
|
13
|
+
source_revision text NOT NULL,
|
|
14
|
+
digest text NOT NULL,
|
|
15
|
+
value jsonb,
|
|
16
|
+
tombstone boolean NOT NULL DEFAULT false,
|
|
17
|
+
deleted_at timestamptz,
|
|
18
|
+
delete_reason text,
|
|
19
|
+
idempotency_key text NOT NULL,
|
|
20
|
+
change_seq bigserial NOT NULL,
|
|
21
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
22
|
+
PRIMARY KEY (tenant, subsystem, path),
|
|
23
|
+
UNIQUE (tenant, subsystem, idempotency_key)
|
|
24
|
+
);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS aiwg_storage_records_change
|
|
26
|
+
ON aiwg_storage_records(tenant, subsystem, change_seq, path);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS aiwg_storage_batch_receipts (
|
|
28
|
+
tenant text NOT NULL,
|
|
29
|
+
subsystem text NOT NULL,
|
|
30
|
+
batch_id uuid NOT NULL,
|
|
31
|
+
payload_digest text NOT NULL,
|
|
32
|
+
high_water_mark bigint NOT NULL,
|
|
33
|
+
receipt jsonb NOT NULL,
|
|
34
|
+
committed_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
35
|
+
PRIMARY KEY (tenant, subsystem, batch_id)
|
|
36
|
+
);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS aiwg_storage_edges (
|
|
38
|
+
tenant text NOT NULL,
|
|
39
|
+
subsystem text NOT NULL,
|
|
40
|
+
source_path text NOT NULL,
|
|
41
|
+
target_path text NOT NULL,
|
|
42
|
+
edge_type text NOT NULL,
|
|
43
|
+
PRIMARY KEY (tenant, subsystem, source_path, target_path, edge_type)
|
|
44
|
+
);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS aiwg_storage_edges_target
|
|
46
|
+
ON aiwg_storage_edges(tenant, subsystem, target_path, edge_type);
|
|
47
|
+
`;
|
|
48
|
+
const POSTGREST_FUNCTIONS = [
|
|
49
|
+
'aiwg_reload_schema_v1()',
|
|
50
|
+
'aiwg_health_v1(text,text)',
|
|
51
|
+
'aiwg_changes_v1(text,text,bigint,integer)',
|
|
52
|
+
'aiwg_snapshot_v1(text,text,integer)',
|
|
53
|
+
'aiwg_query_records_v1(text,text,jsonb,text,integer,boolean)',
|
|
54
|
+
'aiwg_get_record_v1(text,text,text)',
|
|
55
|
+
'aiwg_commit_batch_v1(text,text,uuid,text,jsonb)',
|
|
56
|
+
'aiwg_record_v1(aiwg_storage_records)',
|
|
57
|
+
];
|
|
58
|
+
export async function inspectPostgresSchema(client) {
|
|
59
|
+
const exists = await client.query("SELECT to_regclass('public.aiwg_storage_schema') IS NOT NULL AS exists");
|
|
60
|
+
if (!exists.rows[0]?.exists)
|
|
61
|
+
return { version: 0, records: 0, receipts: 0, edges: 0 };
|
|
62
|
+
const result = await client.query(`
|
|
63
|
+
SELECT schema_version,
|
|
64
|
+
(SELECT count(*) FROM aiwg_storage_records) AS records,
|
|
65
|
+
(SELECT count(*) FROM aiwg_storage_batch_receipts) AS receipts,
|
|
66
|
+
(SELECT count(*) FROM aiwg_storage_edges) AS edges
|
|
67
|
+
FROM aiwg_storage_schema WHERE singleton=true`);
|
|
68
|
+
const row = result.rows[0];
|
|
69
|
+
if (Number(row?.schema_version) !== 1)
|
|
70
|
+
throw new Error(`unsupported PostgreSQL storage schema ${String(row?.schema_version)}`);
|
|
71
|
+
return { version: 1, records: Number(row.records), receipts: Number(row.receipts), edges: Number(row.edges) };
|
|
72
|
+
}
|
|
73
|
+
/** Upgrade an empty database to v1 under a dedicated migration-role connection. */
|
|
74
|
+
export async function upgradePostgresSchemaV1(client) {
|
|
75
|
+
await client.query('BEGIN');
|
|
76
|
+
try {
|
|
77
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended('aiwg-storage-schema',0))");
|
|
78
|
+
const before = await inspectPostgresSchema(client);
|
|
79
|
+
if (before.version === 1) {
|
|
80
|
+
await client.query('COMMIT');
|
|
81
|
+
return before;
|
|
82
|
+
}
|
|
83
|
+
await client.query(POSTGRES_SCHEMA_V1_SQL);
|
|
84
|
+
const after = await inspectPostgresSchema(client);
|
|
85
|
+
if (after.version !== 1)
|
|
86
|
+
throw new Error('PostgreSQL schema upgrade did not reach v1');
|
|
87
|
+
await client.query('COMMIT');
|
|
88
|
+
return after;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
try {
|
|
92
|
+
await client.query('ROLLBACK');
|
|
93
|
+
}
|
|
94
|
+
catch { /* preserve upgrade error */ }
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Roll v1 back to an empty schema. This is intentionally impossible without
|
|
100
|
+
* exact version and observed-count acknowledgements from a prior inspection.
|
|
101
|
+
*/
|
|
102
|
+
export async function rollbackPostgresSchemaV1(client, approval) {
|
|
103
|
+
if (approval.expectedVersion !== 1 || approval.allowDataLoss !== true)
|
|
104
|
+
throw new Error('explicit v1 data-loss approval is required');
|
|
105
|
+
await client.query('BEGIN');
|
|
106
|
+
try {
|
|
107
|
+
await client.query("SELECT pg_advisory_xact_lock(hashtextextended('aiwg-storage-schema',0))");
|
|
108
|
+
const before = await inspectPostgresSchema(client);
|
|
109
|
+
const observed = { records: before.records, receipts: before.receipts, edges: before.edges };
|
|
110
|
+
if (before.version !== 1 || JSON.stringify(observed) !== JSON.stringify(approval.expectedCounts)) {
|
|
111
|
+
throw new Error('schema rollback approval does not match current version/counts');
|
|
112
|
+
}
|
|
113
|
+
for (const signature of POSTGREST_FUNCTIONS)
|
|
114
|
+
await client.query(`DROP FUNCTION IF EXISTS ${signature}`);
|
|
115
|
+
await client.query('DROP TABLE aiwg_storage_edges, aiwg_storage_batch_receipts, aiwg_storage_records, aiwg_storage_schema');
|
|
116
|
+
const after = await inspectPostgresSchema(client);
|
|
117
|
+
if (after.version !== 0)
|
|
118
|
+
throw new Error('PostgreSQL schema rollback did not reach v0');
|
|
119
|
+
await client.query('COMMIT');
|
|
120
|
+
return after;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
try {
|
|
124
|
+
await client.query('ROLLBACK');
|
|
125
|
+
}
|
|
126
|
+
catch { /* preserve rollback error */ }
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function postgresLeastPrivilegeSql(runtimeRole) {
|
|
131
|
+
const role = quoteIdentifier(runtimeRole);
|
|
132
|
+
return `REVOKE ALL ON aiwg_storage_records, aiwg_storage_batch_receipts, aiwg_storage_edges FROM PUBLIC;
|
|
133
|
+
REVOKE ALL ON aiwg_storage_schema FROM PUBLIC;
|
|
134
|
+
GRANT USAGE ON SCHEMA public TO ${role};
|
|
135
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON aiwg_storage_records, aiwg_storage_batch_receipts, aiwg_storage_edges TO ${role};
|
|
136
|
+
GRANT SELECT ON aiwg_storage_schema TO ${role};
|
|
137
|
+
GRANT USAGE, SELECT ON SEQUENCE aiwg_storage_records_change_seq_seq TO ${role};`;
|
|
138
|
+
}
|
|
139
|
+
function quoteIdentifier(value) {
|
|
140
|
+
if (!value || value.length > 63 || /[\u0000-\u001f]/.test(value))
|
|
141
|
+
throw new Error('runtime role must be a printable PostgreSQL identifier up to 63 characters');
|
|
142
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=postgres-schema.js.map
|