@maolon/pi-watcher 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +255 -2
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +369 -0
- package/dist/contracts/interfaces.d.ts +262 -0
- package/dist/contracts/interfaces.js +1 -0
- package/dist/contracts/policy-defaults.json +61 -0
- package/dist/contracts/relay-next.interfaces.d.ts +160 -0
- package/dist/contracts/relay-next.interfaces.js +1 -0
- package/dist/engine/cards.d.ts +68 -0
- package/dist/engine/cards.js +76 -0
- package/dist/engine/engine.d.ts +178 -0
- package/dist/engine/engine.js +1162 -0
- package/dist/engine/hard-rules.d.ts +53 -0
- package/dist/engine/hard-rules.js +96 -0
- package/dist/engine/semantic.d.ts +49 -0
- package/dist/engine/semantic.js +125 -0
- package/dist/engine/service.d.ts +62 -0
- package/dist/engine/service.js +587 -0
- package/dist/engine/tool-actions.d.ts +35 -0
- package/dist/engine/tool-actions.js +348 -0
- package/dist/engine/widget.d.ts +29 -0
- package/dist/engine/widget.js +52 -0
- package/dist/ipc/client.d.ts +26 -0
- package/dist/ipc/client.js +106 -0
- package/dist/ipc/server.d.ts +78 -0
- package/dist/ipc/server.js +105 -0
- package/dist/jev/client.d.ts +38 -0
- package/dist/jev/client.js +239 -0
- package/dist/jev/consent.d.ts +12 -0
- package/dist/jev/consent.js +37 -0
- package/dist/jev/index.d.ts +9 -0
- package/dist/jev/index.js +9 -0
- package/dist/jev/mock.d.ts +30 -0
- package/dist/jev/mock.js +77 -0
- package/dist/jev/pi-registry.d.ts +66 -0
- package/dist/jev/pi-registry.js +127 -0
- package/dist/jev/questions.d.ts +15 -0
- package/dist/jev/questions.js +76 -0
- package/dist/jev/sanitizer.d.ts +10 -0
- package/dist/jev/sanitizer.js +59 -0
- package/dist/jev/types.d.ts +78 -0
- package/dist/jev/types.js +54 -0
- package/dist/pi-extension.d.ts +123 -0
- package/dist/pi-extension.js +687 -0
- package/dist/relay/managed.d.ts +120 -0
- package/dist/relay/managed.js +482 -0
- package/dist/relay/negotiate.d.ts +39 -0
- package/dist/relay/negotiate.js +112 -0
- package/dist/runtime.d.ts +74 -0
- package/dist/runtime.js +246 -0
- package/dist/source/agent-check/adapter.d.ts +57 -0
- package/dist/source/agent-check/adapter.js +224 -0
- package/dist/source/agent-file/adapter.d.ts +57 -0
- package/dist/source/agent-file/adapter.js +217 -0
- package/dist/source/task-status-v1/adapter.d.ts +36 -0
- package/dist/source/task-status-v1/adapter.js +263 -0
- package/dist/source/task-status-v1/producer.d.ts +81 -0
- package/dist/source/task-status-v1/producer.js +127 -0
- package/dist/storage/lock.d.ts +14 -0
- package/dist/storage/lock.js +66 -0
- package/dist/storage/schema.sql +166 -0
- package/dist/storage/store.d.ts +352 -0
- package/dist/storage/store.js +555 -0
- package/dist/util/clock.d.ts +23 -0
- package/dist/util/clock.js +34 -0
- package/dist/util/ids.d.ts +8 -0
- package/dist/util/ids.js +29 -0
- package/dist/util/result.d.ts +28 -0
- package/dist/util/result.js +54 -0
- package/package.json +101 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay 1.2 managed-delivery feature negotiation (design 09 / V0).
|
|
3
|
+
*
|
|
4
|
+
* The watcher owns task semantics; automatic wake goes only through the relay managed path.
|
|
5
|
+
* relay protocol 1.2 (managed receipts / withdraw / scope advance / consumer gate /
|
|
6
|
+
* durable response / applied confirm / audience handle / owner setup) is currently a
|
|
7
|
+
* PROPOSED proposal and is not yet implemented.
|
|
8
|
+
*
|
|
9
|
+
* When negotiation is unavailable: the watcher only provides local observation and display (local-display),
|
|
10
|
+
* and does not silently degrade to a direct wake path without a guard (I05/I21).
|
|
11
|
+
*/
|
|
12
|
+
export const MANAGED_REQUIRED_FEATURES = [
|
|
13
|
+
'source.managed-receipts',
|
|
14
|
+
'event.withdraw',
|
|
15
|
+
'scope.advance',
|
|
16
|
+
'consumer.gate',
|
|
17
|
+
'durable-response',
|
|
18
|
+
'applied-confirm',
|
|
19
|
+
'audience.handle',
|
|
20
|
+
'owner.setup'
|
|
21
|
+
];
|
|
22
|
+
export async function negotiateRelay(options = {}) {
|
|
23
|
+
const checkedAt = new Date().toISOString();
|
|
24
|
+
const probe = options.probeRelay ?? defaultProbe;
|
|
25
|
+
let probed;
|
|
26
|
+
try {
|
|
27
|
+
probed = await probe();
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
probed = { present: false, detail: `probe failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
31
|
+
}
|
|
32
|
+
if (!probed.present) {
|
|
33
|
+
return {
|
|
34
|
+
status: 'unavailable',
|
|
35
|
+
protocolMinor: null,
|
|
36
|
+
features: [],
|
|
37
|
+
requiredFeatures: [...MANAGED_REQUIRED_FEATURES],
|
|
38
|
+
detail: probed.detail ?? 'pi-relay not detected',
|
|
39
|
+
checkedAt,
|
|
40
|
+
transport: 'local-display'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const minor = probed.protocolMinor ?? 1;
|
|
44
|
+
const features = probed.features ?? [];
|
|
45
|
+
if (minor >= 2 && MANAGED_REQUIRED_FEATURES.every(f => features.includes(f))) {
|
|
46
|
+
return {
|
|
47
|
+
status: 'managed-1-2',
|
|
48
|
+
protocolMinor: minor,
|
|
49
|
+
features,
|
|
50
|
+
requiredFeatures: [...MANAGED_REQUIRED_FEATURES],
|
|
51
|
+
detail: 'relay managed path negotiated',
|
|
52
|
+
checkedAt,
|
|
53
|
+
transport: 'relay'
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
status: 'legacy-1-1',
|
|
58
|
+
protocolMinor: minor,
|
|
59
|
+
features,
|
|
60
|
+
requiredFeatures: [...MANAGED_REQUIRED_FEATURES],
|
|
61
|
+
detail: 'relay present without managed-delivery features; legacy display only; auto-resume path disabled (I21)',
|
|
62
|
+
checkedAt,
|
|
63
|
+
transport: 'local-display'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** V1 closed loop: derive the negotiation result from the real state of the watcher's embedded managed source (design 9.5 compatibility mode). */
|
|
67
|
+
export function negotiationFromRelaySource(relay, error) {
|
|
68
|
+
const checkedAt = new Date().toISOString();
|
|
69
|
+
if (!relay) {
|
|
70
|
+
return {
|
|
71
|
+
status: 'unavailable',
|
|
72
|
+
protocolMinor: null,
|
|
73
|
+
features: [],
|
|
74
|
+
requiredFeatures: [...MANAGED_REQUIRED_FEATURES],
|
|
75
|
+
detail: error ?? 'relay source not configured (PI_WATCHER_RELAY=1 to enable)',
|
|
76
|
+
checkedAt,
|
|
77
|
+
transport: 'local-display'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (relay.ready()) {
|
|
81
|
+
return {
|
|
82
|
+
status: 'managed-1-2',
|
|
83
|
+
protocolMinor: 2,
|
|
84
|
+
features: [...MANAGED_REQUIRED_FEATURES],
|
|
85
|
+
requiredFeatures: [...MANAGED_REQUIRED_FEATURES],
|
|
86
|
+
detail: 'watcher managed source provisioned (audience + active scope); transport=relay allowed',
|
|
87
|
+
checkedAt,
|
|
88
|
+
transport: 'relay'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: 'unavailable',
|
|
93
|
+
protocolMinor: 2,
|
|
94
|
+
features: [...MANAGED_REQUIRED_FEATURES],
|
|
95
|
+
requiredFeatures: [...MANAGED_REQUIRED_FEATURES],
|
|
96
|
+
detail: relay.hasActiveMembership()
|
|
97
|
+
? 'relay source open with membership; audience/scope provisioning pending (run relay-setup --finalize)'
|
|
98
|
+
: 'relay source open; session target not bound yet (relay_bindings bind with invite file)',
|
|
99
|
+
checkedAt,
|
|
100
|
+
transport: 'local-display'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Default probe: module presence no longer implies relay availability; only a watcher-side managed source configuration upgrades the transport. */
|
|
104
|
+
async function defaultProbe() {
|
|
105
|
+
// The pi-relay dependency ships with the watcher; the only path to transport=relay is the embedded managed source
|
|
106
|
+
// being ready (PI_WATCHER_RELAY=1 -> negotiationFromRelaySource). When not enabled, honestly report
|
|
107
|
+
// unavailable instead of guessing from module presence (design 9.5; no silent downgrade or silent upgrade).
|
|
108
|
+
return {
|
|
109
|
+
present: false,
|
|
110
|
+
detail: 'managed delivery source not enabled in this runtime (PI_WATCHER_RELAY=1 or options.relay); watcher stays local-display'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime composition layer: store + adapters + engine + service + inspection loop.
|
|
3
|
+
* Shared by the extension and the CLI/service; importing the module has no side effects (design 2.5).
|
|
4
|
+
*/
|
|
5
|
+
import type { JudgePort, ManagedDeliveryPort } from './contracts/interfaces.js';
|
|
6
|
+
import { WatchStore } from './storage/store.js';
|
|
7
|
+
import { WatchEngine, type AttentionNotice, type JudgmentNotice } from './engine/engine.js';
|
|
8
|
+
import { WatchService } from './engine/service.js';
|
|
9
|
+
import { type RelayNegotiation, type ProbeResult } from './relay/negotiate.js';
|
|
10
|
+
import { WatcherRelaySource } from './relay/managed.js';
|
|
11
|
+
import type { Clock } from './util/clock.js';
|
|
12
|
+
import { type PiClassifierRegistry } from './jev/pi-registry.js';
|
|
13
|
+
export interface WatcherRuntimeOptions {
|
|
14
|
+
/** watcher state root (location of SQLite/lock/evidence/results) */
|
|
15
|
+
rootDir: string;
|
|
16
|
+
/** sourceId -> task-status-v1 data root directory */
|
|
17
|
+
sourceRoots: ReadonlyMap<string, string>;
|
|
18
|
+
mode?: 'embedded' | 'service';
|
|
19
|
+
/** Source scope approved by the owner profile */
|
|
20
|
+
allowedSourceIds?: readonly string[];
|
|
21
|
+
/** sourceId -> required check ids (completion contract) */
|
|
22
|
+
requiredCheckIds?: ReadonlyMap<string, readonly string[]>;
|
|
23
|
+
probeRelay?: () => Promise<ProbeResult>;
|
|
24
|
+
/** Inspection interval (default 5s; tests may lower it) */
|
|
25
|
+
pollTickMs?: number;
|
|
26
|
+
/** Pi's model registry (`ctx.modelRegistry`), read live: lets Jev use credentials configured in Pi. */
|
|
27
|
+
piRegistry?: () => PiClassifierRegistry | undefined;
|
|
28
|
+
/** Explicitly injected judge (tests); default: JEV_API_KEY direct client, else Jev via piRegistry, else none */
|
|
29
|
+
judge?: JudgePort;
|
|
30
|
+
/** Consent for outbound data (required for live Jev judgment); default JEV_CONSENT=1 */
|
|
31
|
+
semanticConsent?: boolean;
|
|
32
|
+
/** Relay integration: true or {relayHome} enables the embedded managed source (default relayHome=~/.pi/relay) */
|
|
33
|
+
relay?: boolean | {
|
|
34
|
+
relayHome?: string;
|
|
35
|
+
realm?: string;
|
|
36
|
+
consumerProfileId?: string;
|
|
37
|
+
sourceId?: string;
|
|
38
|
+
};
|
|
39
|
+
/** Attention notification hook (display layer: session toast/widget; does not block the engine) */
|
|
40
|
+
onAttention?: (notice: AttentionNotice) => void;
|
|
41
|
+
/** Judgment-complete notice (reported once to the display layer after an accepted judgment) */
|
|
42
|
+
onJudgment?: (notice: JudgmentNotice) => void;
|
|
43
|
+
/** Injectable clock (tests); default SystemClock */
|
|
44
|
+
clock?: Clock;
|
|
45
|
+
}
|
|
46
|
+
export interface WatcherRuntime {
|
|
47
|
+
store: WatchStore;
|
|
48
|
+
engine: WatchEngine;
|
|
49
|
+
service: WatchService;
|
|
50
|
+
negotiation: RelayNegotiation;
|
|
51
|
+
/** Embedded relay managed source (null when not enabled) */
|
|
52
|
+
relay: WatcherRelaySource | null;
|
|
53
|
+
refreshNegotiation(): RelayNegotiation;
|
|
54
|
+
/** Try to finalize on demand (takes effect right after a mid-session bind; idempotent) */
|
|
55
|
+
ensureRelayReady(): Promise<RelayNegotiation>;
|
|
56
|
+
/** Consume host responses -> applyResponse -> confirmApplied (called automatically inside the loop) */
|
|
57
|
+
pumpRelayResponses(): Promise<void>;
|
|
58
|
+
startLoop(signal?: AbortSignal, onAfterSweep?: () => void): void;
|
|
59
|
+
stopLoop(): void;
|
|
60
|
+
close(): void;
|
|
61
|
+
}
|
|
62
|
+
export declare function startRuntime(options: WatcherRuntimeOptions): Promise<WatcherRuntime>;
|
|
63
|
+
/**
|
|
64
|
+
* Host response pump (T5, design 6.3/7.3): readResponses -> applyResponse -> confirmApplied.
|
|
65
|
+
* The cursor advances only after the whole batch is applied and confirmed; the first failure
|
|
66
|
+
* stops the batch and keeps the cursor, so the next sweep re-reads from the same position.
|
|
67
|
+
* Re-reading is safe: applyResponse dedupes by responseId and confirmApplied is idempotent
|
|
68
|
+
* by its stable operationId (conf-<responseId>). "Forgot it succeeded" must never become
|
|
69
|
+
* "response lost forever" (I16). Failures propagate to the caller instead of being swallowed.
|
|
70
|
+
*/
|
|
71
|
+
export declare function pumpResponses(port: Pick<ManagedDeliveryPort, 'readResponses' | 'confirmApplied'>, service: Pick<WatchService, 'applyResponse'>, store: WatchStore, stream: string): Promise<{
|
|
72
|
+
applied: number;
|
|
73
|
+
cursor: number;
|
|
74
|
+
}>;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime composition layer: store + adapters + engine + service + inspection loop.
|
|
3
|
+
* Shared by the extension and the CLI/service; importing the module has no side effects (design 2.5).
|
|
4
|
+
*/
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import { WatchStore } from './storage/store.js';
|
|
10
|
+
import { TaskStatusV1Adapter } from './source/task-status-v1/adapter.js';
|
|
11
|
+
import { AgentCheckAdapter } from './source/agent-check/adapter.js';
|
|
12
|
+
import { AgentFileAdapter } from './source/agent-file/adapter.js';
|
|
13
|
+
import { WatchEngine, DEFAULT_SEMANTIC_CONFIG } from './engine/engine.js';
|
|
14
|
+
import { WatchService } from './engine/service.js';
|
|
15
|
+
import { negotiateRelay, negotiationFromRelaySource } from './relay/negotiate.js';
|
|
16
|
+
import { WatcherRelaySource, deriveSourceId } from './relay/managed.js';
|
|
17
|
+
import { SystemClock } from './util/clock.js';
|
|
18
|
+
import { JevHttpClient } from './jev/index.js';
|
|
19
|
+
import { PiRegistryJudge } from './jev/pi-registry.js';
|
|
20
|
+
import { jevConsentState } from './jev/consent.js';
|
|
21
|
+
export async function startRuntime(options) {
|
|
22
|
+
const rootDir = path.resolve(options.rootDir);
|
|
23
|
+
const store = await WatchStore.open(rootDir, { mode: options.mode ?? 'embedded' });
|
|
24
|
+
const adapters = new Map();
|
|
25
|
+
for (const [sourceId, sourceRoot] of options.sourceRoots) {
|
|
26
|
+
adapters.set(sourceId, new TaskStatusV1Adapter(path.resolve(sourceRoot)));
|
|
27
|
+
}
|
|
28
|
+
// Generic monitor mode (decision-delta 2026-09-21): only agent-declared evidence sources.
|
|
29
|
+
// unified-exec was removed: the agent holds the log_path from the exec result and watch-file pins
|
|
30
|
+
// the exact path, so no external-library naming heuristics are needed; exit detection = declared pattern matching content.
|
|
31
|
+
adapters.set('agent-file', new AgentFileAdapter());
|
|
32
|
+
adapters.set('agent-check', new AgentCheckAdapter());
|
|
33
|
+
const clock = options.clock ?? new SystemClock();
|
|
34
|
+
// V2 semantic layer: judge selection + consent + policy-defaults thresholds (design 5.5/5.6)
|
|
35
|
+
const policyDefaults = loadPolicyDefaults();
|
|
36
|
+
const semanticConfig = policyDefaults
|
|
37
|
+
? {
|
|
38
|
+
thresholds: policyDefaults.thresholds,
|
|
39
|
+
maxJudgeRequestsPerRootDay: policyDefaults.scheduler.maxJudgeRequestsPerRootDay,
|
|
40
|
+
maxProbesPerInspection: policyDefaults.capacity.maxProbesPerInspection,
|
|
41
|
+
maxProbesPerEpisode: policyDefaults.capacity.maxProbesPerEpisode,
|
|
42
|
+
maxEvidenceEvents: policyDefaults.capacity.maxEvidenceEvents,
|
|
43
|
+
stateMaxBytes: policyDefaults.capacity.stateMaxBytes
|
|
44
|
+
}
|
|
45
|
+
: DEFAULT_SEMANTIC_CONFIG;
|
|
46
|
+
const envKey = typeof process !== 'undefined' ? process.env?.JEV_API_KEY : undefined;
|
|
47
|
+
// Egress consent is read live (env JEV_CONSENT, or the stored /watcher jev consent setting).
|
|
48
|
+
const consent = options.semanticConsent ?? (() => jevConsentState().granted);
|
|
49
|
+
// Judge: an explicit JEV_API_KEY uses the pinned direct client; otherwise Jev through Pi's
|
|
50
|
+
// model registry (TYPESAFE_API_KEY or a provider added via /login); otherwise none.
|
|
51
|
+
const judge = options.judge
|
|
52
|
+
?? (envKey ? new JevHttpClient({ apiKey: envKey })
|
|
53
|
+
: options.piRegistry ? new PiRegistryJudge(options.piRegistry) : undefined);
|
|
54
|
+
// V1 closed loop: embedded relay managed source (PI_WATCHER_RELAY=1 or options.relay)
|
|
55
|
+
const relayEnabled = options.relay === true || (typeof options.relay === 'object' && options.relay !== undefined)
|
|
56
|
+
|| (typeof process !== 'undefined' && process.env?.PI_WATCHER_RELAY === '1');
|
|
57
|
+
let relay = null;
|
|
58
|
+
let relayError;
|
|
59
|
+
if (relayEnabled) {
|
|
60
|
+
const relayOpts = typeof options.relay === 'object' && options.relay !== undefined ? options.relay : {};
|
|
61
|
+
try {
|
|
62
|
+
relay = await WatcherRelaySource.open(path.join(rootDir, 'relay'), {
|
|
63
|
+
relayHome: relayOpts.relayHome ?? process.env?.PI_RELAY_HOME ?? path.join(os.homedir(), '.pi', 'relay'),
|
|
64
|
+
realm: relayOpts.realm,
|
|
65
|
+
consumerProfileId: relayOpts.consumerProfileId,
|
|
66
|
+
// One source store per root: sharing a global sourceId across roots would let an unrelated session's owner lock
|
|
67
|
+
// block roots that really need relay (2026-09-21); an explicitly passed value is respected
|
|
68
|
+
sourceId: relayOpts.sourceId ?? deriveSourceId(rootDir)
|
|
69
|
+
});
|
|
70
|
+
// Binding exists but not configured -> configure automatically (owner pre-authorized via relay-setup; design 9.4 audience)
|
|
71
|
+
if (!relay.ready() && relay.hasActiveMembership()) {
|
|
72
|
+
try {
|
|
73
|
+
relay.ensureConsumerDeclaration();
|
|
74
|
+
relay.provision();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* left for the owner to handle with relay-setup --finalize; inspection continues in local-display */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
relayError = e instanceof Error ? e.message : String(e);
|
|
83
|
+
relay = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// When open throws, relay=null but relayError holds the real cause (authority mismatch / owner_conflict / ...);
|
|
87
|
+
// do not fall back to the default probe message (it would mask the real failure as "relay not enabled")
|
|
88
|
+
const negotiation0 = relayError
|
|
89
|
+
? negotiationFromRelaySource(null, relayError)
|
|
90
|
+
: relay
|
|
91
|
+
? negotiationFromRelaySource(relay)
|
|
92
|
+
: await negotiateRelay(options.probeRelay ? { probeRelay: options.probeRelay } : {});
|
|
93
|
+
let negotiation = negotiation0;
|
|
94
|
+
const delivery = () => (relay && relay.ready() ? relay : undefined);
|
|
95
|
+
const engine = new WatchEngine({
|
|
96
|
+
clock,
|
|
97
|
+
store,
|
|
98
|
+
adapters,
|
|
99
|
+
requiredCheckIds: options.requiredCheckIds ?? new Map(),
|
|
100
|
+
judge,
|
|
101
|
+
semantic: semanticConfig,
|
|
102
|
+
semanticConsent: consent,
|
|
103
|
+
judgeRequiresConsent: !options.judge && !!judge,
|
|
104
|
+
delivery,
|
|
105
|
+
onAttention: options.onAttention,
|
|
106
|
+
onJudgment: options.onJudgment
|
|
107
|
+
});
|
|
108
|
+
const service = new WatchService({
|
|
109
|
+
clock,
|
|
110
|
+
store,
|
|
111
|
+
engine,
|
|
112
|
+
negotiation: () => negotiation,
|
|
113
|
+
allowedSourceIds: options.allowedSourceIds ?? [...options.sourceRoots.keys(), 'agent-file', 'agent-check'],
|
|
114
|
+
requiredCheckIds: options.requiredCheckIds ?? new Map(),
|
|
115
|
+
delivery: () => (relay && relay.ready() ? relay : undefined)
|
|
116
|
+
});
|
|
117
|
+
let timer = null;
|
|
118
|
+
const RESPONSE_STREAM = 'managed-responses';
|
|
119
|
+
const refreshNegotiation = () => {
|
|
120
|
+
if (relay)
|
|
121
|
+
negotiation = negotiationFromRelaySource(relay);
|
|
122
|
+
return negotiation;
|
|
123
|
+
};
|
|
124
|
+
const ensureRelayReady = async () => {
|
|
125
|
+
if (relay) {
|
|
126
|
+
// resume self-heal: local setup claims ready but relay truth is closed (resume scenario)
|
|
127
|
+
// -> reconfigure with a fresh audienceRef/scopeId (needs an active membership; with no binding, retry after auto-bind)
|
|
128
|
+
if (relay.ready()) {
|
|
129
|
+
try {
|
|
130
|
+
const healed = relay.ensureHealed();
|
|
131
|
+
if (healed.healed)
|
|
132
|
+
refreshNegotiation();
|
|
133
|
+
}
|
|
134
|
+
catch { /* diagnosis/reconfigure failed: keep the status quo and retry next round (delivery failures have a separate make-loud fallback) */ }
|
|
135
|
+
}
|
|
136
|
+
else if (relay.hasActiveMembership()) {
|
|
137
|
+
try {
|
|
138
|
+
relay.ensureConsumerDeclaration();
|
|
139
|
+
relay.provision();
|
|
140
|
+
refreshNegotiation();
|
|
141
|
+
}
|
|
142
|
+
catch { /* retry on the next loop */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return negotiation;
|
|
146
|
+
};
|
|
147
|
+
const runtime = {
|
|
148
|
+
store,
|
|
149
|
+
engine,
|
|
150
|
+
service,
|
|
151
|
+
// Live value rather than a startup snapshot: once standing binding/provision completes, negotiation flips from unavailable to
|
|
152
|
+
// managed-1-2; a snapshot would pin agent-registered watches to local-display forever (observed 2026-09-21)
|
|
153
|
+
get negotiation() { return negotiation; },
|
|
154
|
+
relay,
|
|
155
|
+
refreshNegotiation,
|
|
156
|
+
ensureRelayReady,
|
|
157
|
+
async pumpRelayResponses() {
|
|
158
|
+
if (!relay || !relay.ready())
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
await pumpResponses(relay, service, store, RESPONSE_STREAM);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* Relay unreachable or a response failed: local observation continues (design 9.5);
|
|
165
|
+
the cursor was not advanced, so the next sweep re-reads the same batch. */
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
startLoop(signal, onAfterSweep) {
|
|
169
|
+
if (timer)
|
|
170
|
+
return;
|
|
171
|
+
const tick = options.pollTickMs ?? 5000;
|
|
172
|
+
timer = setInterval(() => {
|
|
173
|
+
if (signal?.aborted) {
|
|
174
|
+
runtime.stopLoop();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Auto-finalize after a mid-session bind (owner pre-authorized audience/scope via relay-setup)
|
|
178
|
+
void ensureRelayReady();
|
|
179
|
+
void engine.runDue().catch(() => {
|
|
180
|
+
/* an inspection failure does not interrupt the loop; health is degraded internally by the engine */
|
|
181
|
+
});
|
|
182
|
+
void runtime.pumpRelayResponses().catch(() => { });
|
|
183
|
+
// Widget refresh (design 11 s33): re-pull the status-bar projection after every tick; it does not enter the LLM context
|
|
184
|
+
try {
|
|
185
|
+
onAfterSweep?.();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* display-layer failure does not affect the engine */
|
|
189
|
+
}
|
|
190
|
+
}, tick);
|
|
191
|
+
timer.unref?.();
|
|
192
|
+
},
|
|
193
|
+
stopLoop() {
|
|
194
|
+
if (timer) {
|
|
195
|
+
clearInterval(timer);
|
|
196
|
+
timer = null;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
close() {
|
|
200
|
+
runtime.stopLoop();
|
|
201
|
+
for (const adapter of adapters.values()) {
|
|
202
|
+
void adapter.close();
|
|
203
|
+
}
|
|
204
|
+
store.close();
|
|
205
|
+
if (relay)
|
|
206
|
+
void relay.close();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
return runtime;
|
|
210
|
+
}
|
|
211
|
+
function loadPolicyDefaults() {
|
|
212
|
+
// Resolved next to this module only (src/ under tsx, dist/ when installed). Never from the
|
|
213
|
+
// working directory: a user project must not be able to silently override the thresholds.
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(readFileSync(fileURLToPath(new URL('./contracts/policy-defaults.json', import.meta.url)), 'utf8'));
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Host response pump (T5, design 6.3/7.3): readResponses -> applyResponse -> confirmApplied.
|
|
223
|
+
* The cursor advances only after the whole batch is applied and confirmed; the first failure
|
|
224
|
+
* stops the batch and keeps the cursor, so the next sweep re-reads from the same position.
|
|
225
|
+
* Re-reading is safe: applyResponse dedupes by responseId and confirmApplied is idempotent
|
|
226
|
+
* by its stable operationId (conf-<responseId>). "Forgot it succeeded" must never become
|
|
227
|
+
* "response lost forever" (I16). Failures propagate to the caller instead of being swallowed.
|
|
228
|
+
*/
|
|
229
|
+
export async function pumpResponses(port, service, store, stream) {
|
|
230
|
+
let after = store.transaction(tx => tx.getRelayCursor?.(stream)) ?? 0;
|
|
231
|
+
if (typeof after !== 'number')
|
|
232
|
+
after = 0;
|
|
233
|
+
const batch = await port.readResponses(after);
|
|
234
|
+
let applied = 0;
|
|
235
|
+
for (const response of batch.responses) {
|
|
236
|
+
const result = await service.applyResponse(response);
|
|
237
|
+
await port.confirmApplied(`conf-${response.responseId}`, response.responseId, result);
|
|
238
|
+
applied += 1;
|
|
239
|
+
}
|
|
240
|
+
if (batch.cursor > after) {
|
|
241
|
+
store.transaction(tx => {
|
|
242
|
+
tx.putRelayCursor?.(stream, batch.cursor);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return { applied, cursor: Math.max(after, batch.cursor) };
|
|
246
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-check SourceAdapter — agent-declared bounded check command
|
|
3
|
+
* (agent-declared check source).
|
|
4
|
+
*
|
|
5
|
+
* Target: { kind:'run', sourceId:'agent-check', taskId:'check',
|
|
6
|
+
* runId:'chk-<digest16>', attemptId:'attempt-1', check:{...} }
|
|
7
|
+
*
|
|
8
|
+
* The adapter RUNS the declared command at the watch's poll cadence (engine
|
|
9
|
+
* schedules read(); read executes). This is bounded probing, never a business
|
|
10
|
+
* action: timeoutMs hard kill (SIGTERM→SIGKILL 2s), byte-capped capture.
|
|
11
|
+
*
|
|
12
|
+
* Verdict mapping (nonzero exit is three-valued; agent declares the mapping):
|
|
13
|
+
* - broken : spawn error / exit 126|127 / timeout → state 'unknown' + gap
|
|
14
|
+
* (health degraded) — the CHECK is broken; never a task verdict.
|
|
15
|
+
* - failed : exitCode ∈ failCodes OR failPattern matches stdout — explicit
|
|
16
|
+
* declared failure evidence.
|
|
17
|
+
* - succeeded: okPattern declared → match required; otherwise exit 0
|
|
18
|
+
* (marker philosophy: exit code is explicit evidence).
|
|
19
|
+
* - pending : any other outcome → 'running' (condition not yet met).
|
|
20
|
+
* Ordering: broken → declared-fail → declared-ok → pending.
|
|
21
|
+
*
|
|
22
|
+
* Evidence economy: log_delta only when output digest changes ("no new
|
|
23
|
+
* information" is the Jev signal, not a journal row); status only on state
|
|
24
|
+
* change. Silence therefore means "output and state unchanged for
|
|
25
|
+
* maxSilenceMs" — stuck-pending surfaces via the silence episode.
|
|
26
|
+
*
|
|
27
|
+
* Single-flight per target: overlapping sweeps skip (never double-run).
|
|
28
|
+
*/
|
|
29
|
+
import type { Json, Observation, Probe, RunTarget, SourceAdapter, SourceCapabilities } from '../../contracts/interfaces.js';
|
|
30
|
+
export interface AgentCheckCursor {
|
|
31
|
+
lastSeq: number;
|
|
32
|
+
lastState: string;
|
|
33
|
+
lastOutputDigest: string;
|
|
34
|
+
/** terminal already reported (succeeded/failed) → stop running the command */
|
|
35
|
+
done: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare class AgentCheckAdapter implements SourceAdapter {
|
|
38
|
+
readonly adapterId = "agent-check";
|
|
39
|
+
readonly capabilities: SourceCapabilities;
|
|
40
|
+
private readonly inflight;
|
|
41
|
+
private runCheck;
|
|
42
|
+
private verdictOf;
|
|
43
|
+
read(target: RunTarget, cursor: Json | undefined, _signal: AbortSignal): Promise<{
|
|
44
|
+
observations: Observation[];
|
|
45
|
+
nextCursor: Json;
|
|
46
|
+
gap: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
probes(_target: RunTarget, _basis: unknown): Promise<readonly Probe[]>;
|
|
49
|
+
execute(_probe: Probe, _signal: AbortSignal): Promise<Observation[]>;
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** runId of the watch-check tool action: stable digest of the command + verdict mapping (for dedup) */
|
|
53
|
+
export declare function checkRunId(cmd: string, verdict: {
|
|
54
|
+
okPattern?: string;
|
|
55
|
+
failPattern?: string;
|
|
56
|
+
failCodes?: number[];
|
|
57
|
+
}): string;
|