attesso 0.7.0 → 0.8.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/README.md +3 -1
- package/bin/attesso.mjs +27 -9
- package/package.json +1 -1
- package/src/sync.mjs +38 -1
package/README.md
CHANGED
|
@@ -42,11 +42,13 @@ export ATTESSO_SYNC_URL=https://api.attesso.com
|
|
|
42
42
|
export ATTESSO_API_KEY=att_live_...
|
|
43
43
|
npx attesso sync --dry-run
|
|
44
44
|
npx attesso sync
|
|
45
|
+
npx attesso sync --watch
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
The API key must belong to an entitled live workspace. A retry after a failed
|
|
48
49
|
request is safe: the server deduplicates by device and sequence, and the local
|
|
49
|
-
cursor does not move until the batch is accepted.
|
|
50
|
+
cursor does not move until the batch is accepted. `--watch` repeats every five
|
|
51
|
+
seconds, so the hosted view follows a running agent without another command.
|
|
50
52
|
|
|
51
53
|
## The overview
|
|
52
54
|
|
package/bin/attesso.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { resolve } from 'node:path';
|
|
|
29
29
|
import { CAPABILITIES, TRUST_LEVELS, validateRules } from './../src/rules.mjs';
|
|
30
30
|
import { validatePathsConfig } from './../src/fs-paths.mjs';
|
|
31
31
|
import { startUi } from './../src/ui/server.mjs';
|
|
32
|
-
import { sync } from './../src/sync.mjs';
|
|
32
|
+
import { sync, watchSync } from './../src/sync.mjs';
|
|
33
33
|
|
|
34
34
|
const argv = process.argv.slice(2);
|
|
35
35
|
const dirAt = argv.indexOf('--dir');
|
|
@@ -77,8 +77,9 @@ const USAGE = `attesso: a local, signed ledger of what your agents attempted
|
|
|
77
77
|
attesso mode [observe|enforce|off] read or set the active hook mode
|
|
78
78
|
attesso paths [--json|--set FILE] read or replace the filesystem scope
|
|
79
79
|
attesso ui [--port N] [--no-open] local control surface for the signed ledger
|
|
80
|
-
attesso sync [--url URL] [--limit N] [--json] [--dry-run]
|
|
80
|
+
attesso sync [--url URL] [--limit N] [--json] [--dry-run] [--watch]
|
|
81
81
|
send the next decisions to the hosted mirror
|
|
82
|
+
--watch keeps it current every 5 seconds
|
|
82
83
|
(key from ATTESSO_API_KEY; never printed)
|
|
83
84
|
attesso explain <seq> one entry in full: rule, tainting step, what to do
|
|
84
85
|
attesso suggest [--json] rules the ledger's own counts justify
|
|
@@ -628,13 +629,30 @@ switch (command) {
|
|
|
628
629
|
try {
|
|
629
630
|
const options = flags(rest);
|
|
630
631
|
const requested = options.limit === undefined ? 50 : Number(options.limit);
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
632
|
+
const url = typeof options.url === 'string' ? options.url : process.env.ATTESSO_SYNC_URL;
|
|
633
|
+
const key = typeof options.key === 'string' ? options.key : process.env.ATTESSO_API_KEY;
|
|
634
|
+
if (rest.includes('--watch')) {
|
|
635
|
+
if (rest.includes('--dry-run')) throw new Error('--watch cannot be combined with --dry-run');
|
|
636
|
+
const controller = new AbortController();
|
|
637
|
+
process.on('SIGINT', () => controller.abort());
|
|
638
|
+
process.on('SIGTERM', () => controller.abort());
|
|
639
|
+
console.log('sync: watching the ledger; Ctrl-C stops');
|
|
640
|
+
await watchSync({
|
|
641
|
+
dir: DIR,
|
|
642
|
+
url,
|
|
643
|
+
key,
|
|
644
|
+
limit: requested,
|
|
645
|
+
intervalMs: options.interval === undefined ? 5000 : Number(options.interval),
|
|
646
|
+
signal: controller.signal,
|
|
647
|
+
onResult: (result) => {
|
|
648
|
+
if (result instanceof Error) console.error(`sync: ${result.message}`);
|
|
649
|
+
else if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
650
|
+
else console.log(`sync: ${result.accepted} accepted, ${result.skipped} already present, ${result.metered} metered; last #${result.last_seq}`);
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
const result = await sync({ dir: DIR, url, key, limit: requested, dryRun: rest.includes('--dry-run') });
|
|
638
656
|
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
639
657
|
else if (result.dry_run) console.log(`sync dry run: ${result.entries} decision(s) ready`);
|
|
640
658
|
else console.log(`sync: ${result.accepted} accepted, ${result.skipped} already present, ${result.metered} metered; last #${result.last_seq}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "attesso",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Bounds: decide an AI agent's next tool call before it runs, with a signed record. Free and unlimited locally, $0.00085 per synced decision.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/sync.mjs
CHANGED
|
@@ -60,7 +60,10 @@ export async function sync({
|
|
|
60
60
|
if (typeof deviceId !== 'string' || !/^[A-Za-z0-9._:-]{1,64}$/.test(deviceId)) {
|
|
61
61
|
throw new Error('ledger key id is not a valid device id');
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
let state = readSyncState(dir);
|
|
64
|
+
if (state.device_id && state.device_id !== deviceId) {
|
|
65
|
+
state = { version: SYNC_VERSION, last_seq: 0 };
|
|
66
|
+
}
|
|
64
67
|
const entries = [];
|
|
65
68
|
forEachEntrySync(ledgerPath, (entry) => {
|
|
66
69
|
if (entry.seq <= state.last_seq) return;
|
|
@@ -112,3 +115,37 @@ export async function sync({
|
|
|
112
115
|
writeSyncState(dir, lastSeq, deviceId);
|
|
113
116
|
return { accepted, skipped, metered, last_seq: lastSeq, device_id: deviceId };
|
|
114
117
|
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
// watchSync keeps the hosted mirror current for a long-running agent session.
|
|
121
|
+
// It never advances the cursor on an error; the next tick retries the same
|
|
122
|
+
// window. Callers stop it with an AbortSignal.
|
|
123
|
+
export async function watchSync({
|
|
124
|
+
dir,
|
|
125
|
+
url,
|
|
126
|
+
key,
|
|
127
|
+
limit = DEFAULT_LIMIT,
|
|
128
|
+
intervalMs = 5000,
|
|
129
|
+
signal,
|
|
130
|
+
onResult = () => {},
|
|
131
|
+
} = {}) {
|
|
132
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 1000 || intervalMs > 300_000) {
|
|
133
|
+
throw new Error('interval must be an integer from 1000 to 300000 milliseconds');
|
|
134
|
+
}
|
|
135
|
+
while (!signal?.aborted) {
|
|
136
|
+
try {
|
|
137
|
+
const result = await sync({ dir, url, key, limit });
|
|
138
|
+
if (result.accepted || result.skipped || result.metered) onResult(result);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
onResult(error);
|
|
141
|
+
}
|
|
142
|
+
if (signal?.aborted) break;
|
|
143
|
+
await new Promise((resolve) => {
|
|
144
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
145
|
+
signal?.addEventListener('abort', () => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
resolve();
|
|
148
|
+
}, { once: true });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|