@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2
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 +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { text, isNotification } from './validation.js';
|
|
2
|
+
import { createNonce, nowIso } from './store.js';
|
|
3
|
+
import { canonicalJson } from './envelope.js';
|
|
4
|
+
// Bounded admission for hook/plugin notifications.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: a hook that fires on every event can wake a model in a loop, a
|
|
7
|
+
// session can notify itself, and an approval request must never be answered by another
|
|
8
|
+
// agent. This gate answers one question — may this notification be delivered? — and it
|
|
9
|
+
// records the decision in the task record. It does not send anything.
|
|
10
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
11
|
+
per_key: 3,
|
|
12
|
+
window_ms: 60_000,
|
|
13
|
+
max_depth: 2,
|
|
14
|
+
max_entries: 200,
|
|
15
|
+
});
|
|
16
|
+
export const NOTIFICATION_KINDS = Object.freeze([
|
|
17
|
+
'task_event',
|
|
18
|
+
'turn_complete',
|
|
19
|
+
'tool_failure',
|
|
20
|
+
'approval_request',
|
|
21
|
+
'status_probe',
|
|
22
|
+
'handoff_note',
|
|
23
|
+
]);
|
|
24
|
+
function assert(condition, field) {
|
|
25
|
+
if (!condition)
|
|
26
|
+
throw new Error(`Invalid notification field: ${field}`);
|
|
27
|
+
}
|
|
28
|
+
export function validateNotification(notification) {
|
|
29
|
+
assert(isNotification(notification), 'notification');
|
|
30
|
+
assert(notification.schema === 'harness-collab/notification/1', 'notification.schema');
|
|
31
|
+
assert(text(notification.kind), 'notification.kind');
|
|
32
|
+
assert(NOTIFICATION_KINDS.includes(notification.kind), 'notification.kind value');
|
|
33
|
+
assert(text(notification.origin_session), 'notification.origin_session');
|
|
34
|
+
assert(text(notification.target_session), 'notification.target_session');
|
|
35
|
+
assert(Number.isSafeInteger(notification.depth ?? 0) &&
|
|
36
|
+
(notification.depth ?? 0) >= 0, 'notification.depth');
|
|
37
|
+
assert(text(notification.task_id), 'notification.task_id');
|
|
38
|
+
return notification;
|
|
39
|
+
}
|
|
40
|
+
/** Stable identity of a notification: everything that decides whether it is the same one. */
|
|
41
|
+
export function notificationKey(notification) {
|
|
42
|
+
return canonicalJson({
|
|
43
|
+
kind: notification.kind,
|
|
44
|
+
task_id: notification.task_id,
|
|
45
|
+
subtask_id: notification.subtask_id ?? null,
|
|
46
|
+
attempt: notification.attempt ?? null,
|
|
47
|
+
origin_session: notification.origin_session,
|
|
48
|
+
event: notification.event ?? null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decide whether to deliver. Returns `{admit, reason, human_required}`. An approval
|
|
53
|
+
* request is recorded but never answerable by another agent, and no decision here
|
|
54
|
+
* triggers a send, a retry or a model wake.
|
|
55
|
+
*/
|
|
56
|
+
export function admitNotification(record, notification, { clock = Date.now(), limits = {} } = {}) {
|
|
57
|
+
validateNotification(notification);
|
|
58
|
+
const effective = { ...DEFAULT_LIMITS, ...limits };
|
|
59
|
+
const at = nowIso(clock);
|
|
60
|
+
const key = notificationKey(notification);
|
|
61
|
+
const entry = {
|
|
62
|
+
notification_id: `note-${createNonce()}`,
|
|
63
|
+
key,
|
|
64
|
+
kind: notification.kind,
|
|
65
|
+
task_id: notification.task_id,
|
|
66
|
+
subtask_id: notification.subtask_id ?? null,
|
|
67
|
+
attempt: notification.attempt ?? null,
|
|
68
|
+
origin_session: notification.origin_session,
|
|
69
|
+
target_session: notification.target_session,
|
|
70
|
+
depth: notification.depth ?? 0,
|
|
71
|
+
at,
|
|
72
|
+
decision: 'admitted',
|
|
73
|
+
reason: null,
|
|
74
|
+
};
|
|
75
|
+
const log = record.notifications ?? (record.notifications = []);
|
|
76
|
+
if (notification.kind === 'approval_request') {
|
|
77
|
+
// Recorded and surfaced, never answered, and never used to wake another model.
|
|
78
|
+
log.push({
|
|
79
|
+
...entry,
|
|
80
|
+
decision: 'recorded',
|
|
81
|
+
reason: 'approval_requests_are_not_auto_answered',
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
admit: false,
|
|
85
|
+
reason: 'approval_requests_are_not_auto_answered',
|
|
86
|
+
human_required: true,
|
|
87
|
+
key,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (notification.origin_session === notification.target_session) {
|
|
91
|
+
log.push({ ...entry, decision: 'dropped', reason: 'self_notification' });
|
|
92
|
+
return {
|
|
93
|
+
admit: false,
|
|
94
|
+
reason: 'self_notification',
|
|
95
|
+
human_required: false,
|
|
96
|
+
key,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if ((notification.depth ?? 0) > effective.max_depth) {
|
|
100
|
+
log.push({
|
|
101
|
+
...entry,
|
|
102
|
+
decision: 'dropped',
|
|
103
|
+
reason: 'recursion_depth_exceeded',
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
admit: false,
|
|
107
|
+
reason: 'recursion_depth_exceeded',
|
|
108
|
+
human_required: false,
|
|
109
|
+
key,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const windowStart = clock - effective.window_ms;
|
|
113
|
+
const recent = log.filter((item) => item.key === key &&
|
|
114
|
+
item.decision === 'admitted' &&
|
|
115
|
+
Number.isFinite(Date.parse(item.at)) &&
|
|
116
|
+
Date.parse(item.at) >= windowStart);
|
|
117
|
+
if (recent.length >= effective.per_key) {
|
|
118
|
+
log.push({ ...entry, decision: 'dropped', reason: 'rate_limited' });
|
|
119
|
+
return {
|
|
120
|
+
admit: false,
|
|
121
|
+
reason: 'rate_limited',
|
|
122
|
+
delivered_in_window: recent.length,
|
|
123
|
+
key,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
log.push(entry);
|
|
127
|
+
return { admit: true, reason: null, human_required: false, key };
|
|
128
|
+
}
|
|
129
|
+
/** Keep the notification log bounded; the newest entries are the ones that matter. */
|
|
130
|
+
export function trimNotificationLog(record, { limits = {} } = {}) {
|
|
131
|
+
const effective = { ...DEFAULT_LIMITS, ...limits };
|
|
132
|
+
if (!Array.isArray(record.notifications))
|
|
133
|
+
return 0;
|
|
134
|
+
const excess = record.notifications.length - effective.max_entries;
|
|
135
|
+
if (excess <= 0)
|
|
136
|
+
return 0;
|
|
137
|
+
record.notifications = record.notifications.slice(excess);
|
|
138
|
+
record.notifications_trimmed = (record.notifications_trimmed ?? 0) + excess;
|
|
139
|
+
return excess;
|
|
140
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Assignment, CapabilityCard } from './contracts.js';
|
|
2
|
+
export interface Check {
|
|
3
|
+
code: string;
|
|
4
|
+
capability?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function validateContractAndCard(contract: Assignment, card: CapabilityCard): void;
|
|
7
|
+
/** Validate reported readiness, not authorization, resource locking, or task completion. */
|
|
8
|
+
export declare function preflight(contract: Assignment, card: CapabilityCard, { now, maxAgeMinutes }?: {
|
|
9
|
+
maxAgeMinutes?: number | undefined;
|
|
10
|
+
now?: number | undefined;
|
|
11
|
+
}): {
|
|
12
|
+
status: string;
|
|
13
|
+
errors: string[];
|
|
14
|
+
blockers: never[];
|
|
15
|
+
warnings: never[];
|
|
16
|
+
task_id?: never;
|
|
17
|
+
assignment_id?: never;
|
|
18
|
+
attempt?: never;
|
|
19
|
+
checked_at?: never;
|
|
20
|
+
limits?: never;
|
|
21
|
+
} | {
|
|
22
|
+
status: string;
|
|
23
|
+
task_id: string;
|
|
24
|
+
assignment_id: string;
|
|
25
|
+
attempt: number;
|
|
26
|
+
checked_at: string;
|
|
27
|
+
errors: never[];
|
|
28
|
+
blockers: Check[];
|
|
29
|
+
warnings: Check[];
|
|
30
|
+
limits: string[];
|
|
31
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { object, text } from './validation.js';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fingerprintSkill } from './fingerprint.js';
|
|
6
|
+
const record = object;
|
|
7
|
+
const digest = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
8
|
+
const positive = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
9
|
+
const states = new Set([
|
|
10
|
+
'declared',
|
|
11
|
+
'discovered',
|
|
12
|
+
'callable',
|
|
13
|
+
'verified',
|
|
14
|
+
'disabled',
|
|
15
|
+
'unknown',
|
|
16
|
+
]);
|
|
17
|
+
const readyStates = new Set(['callable', 'verified']);
|
|
18
|
+
function assert(condition, field) {
|
|
19
|
+
if (!condition)
|
|
20
|
+
throw new Error(`Invalid field: ${field}`);
|
|
21
|
+
}
|
|
22
|
+
function requirements(value, field) {
|
|
23
|
+
assert(record(value), field);
|
|
24
|
+
assert(Array.isArray(value.skills) && Array.isArray(value.tools), field);
|
|
25
|
+
const keys = new Set();
|
|
26
|
+
for (const skill of value.skills) {
|
|
27
|
+
assert(record(skill) && text(skill.id) && digest(skill.bundle_digest), `${field}.skills`);
|
|
28
|
+
assert(!keys.has(`skill:${skill.id}`), `${field}.skills duplicate`);
|
|
29
|
+
keys.add(`skill:${skill.id}`);
|
|
30
|
+
}
|
|
31
|
+
for (const tool of value.tools) {
|
|
32
|
+
assert(text(tool) && !keys.has(`tool:${tool}`), `${field}.tools`);
|
|
33
|
+
keys.add(`tool:${tool}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function validateContractAndCard(contract, card) {
|
|
37
|
+
assert(record(contract), 'contract');
|
|
38
|
+
assert(contract.schema === 'harness-collab/assignment/1', 'contract.schema');
|
|
39
|
+
for (const key of ['task_id', 'assignment_id', 'objective'])
|
|
40
|
+
assert(text(contract[key]), `contract.${key}`);
|
|
41
|
+
assert(positive(contract.attempt), 'contract.attempt');
|
|
42
|
+
assert(Array.isArray(contract.roles) &&
|
|
43
|
+
contract.roles.length > 0 &&
|
|
44
|
+
contract.roles.every(text), 'contract.roles');
|
|
45
|
+
assert(Array.isArray(contract.acceptance) &&
|
|
46
|
+
contract.acceptance.length > 0 &&
|
|
47
|
+
contract.acceptance.every(text), 'contract.acceptance');
|
|
48
|
+
assert(record(contract.coordinator) &&
|
|
49
|
+
text(contract.coordinator.agent_id) &&
|
|
50
|
+
text(contract.coordinator.session_id) &&
|
|
51
|
+
positive(contract.coordinator.owner_epoch), 'contract.coordinator');
|
|
52
|
+
assert(record(contract.assignee), 'contract.assignee');
|
|
53
|
+
for (const key of [
|
|
54
|
+
'agent_id',
|
|
55
|
+
'session_id',
|
|
56
|
+
'harness',
|
|
57
|
+
'surface',
|
|
58
|
+
'configuration_id',
|
|
59
|
+
]) {
|
|
60
|
+
assert(text(contract.assignee[key]), `contract.assignee.${key}`);
|
|
61
|
+
}
|
|
62
|
+
assert(text(contract.assignee.cwd) && path.isAbsolute(contract.assignee.cwd), 'contract.assignee.cwd');
|
|
63
|
+
assert(Array.isArray(contract.inputs) && contract.inputs.length > 0, 'contract.inputs');
|
|
64
|
+
for (const input of contract.inputs) {
|
|
65
|
+
assert(record(input) &&
|
|
66
|
+
text(input.ref) &&
|
|
67
|
+
path.isAbsolute(input.ref) &&
|
|
68
|
+
digest(input.digest), 'contract.inputs item');
|
|
69
|
+
}
|
|
70
|
+
assert(Array.isArray(contract.resources), 'contract.resources');
|
|
71
|
+
for (const resource of contract.resources) {
|
|
72
|
+
assert(record(resource) &&
|
|
73
|
+
text(resource.path) &&
|
|
74
|
+
path.isAbsolute(resource.path) &&
|
|
75
|
+
['read', 'write'].includes(resource.access), 'contract.resources item');
|
|
76
|
+
if (resource.access === 'write')
|
|
77
|
+
assert(positive(resource.resource_epoch), 'contract.resources.resource_epoch');
|
|
78
|
+
}
|
|
79
|
+
assert(record(contract.runner) &&
|
|
80
|
+
text(contract.runner.name) &&
|
|
81
|
+
text(contract.runner.mode) &&
|
|
82
|
+
Array.isArray(contract.runner.omitted_capabilities) &&
|
|
83
|
+
contract.runner.omitted_capabilities.every(text), 'contract.runner');
|
|
84
|
+
requirements(contract.required, 'contract.required');
|
|
85
|
+
requirements(contract.preferred, 'contract.preferred');
|
|
86
|
+
assert(record(card) && card.schema === 'harness-collab/capabilities/1', 'capabilities.schema');
|
|
87
|
+
for (const key of [
|
|
88
|
+
'agent_id',
|
|
89
|
+
'session_id',
|
|
90
|
+
'harness',
|
|
91
|
+
'surface',
|
|
92
|
+
'configuration_id',
|
|
93
|
+
]) {
|
|
94
|
+
assert(text(card[key]), `capabilities.${key}`);
|
|
95
|
+
}
|
|
96
|
+
assert(text(card.cwd) && path.isAbsolute(card.cwd), 'capabilities.cwd');
|
|
97
|
+
assert(text(card.observed_at) &&
|
|
98
|
+
/^\d{4}-\d\d-\d\dT.*(?:Z|[+-]\d\d:\d\d)$/.test(card.observed_at) &&
|
|
99
|
+
Number.isFinite(Date.parse(card.observed_at)), 'capabilities.observed_at');
|
|
100
|
+
assert(['runtime', 'declared'].includes(card.evidence_level), 'capabilities.evidence_level');
|
|
101
|
+
if (card.evidence_records !== undefined) {
|
|
102
|
+
assert(Array.isArray(card.evidence_records) && card.evidence_records.length > 0, 'capabilities.evidence_records');
|
|
103
|
+
for (const item of card.evidence_records) {
|
|
104
|
+
assert(record(item) &&
|
|
105
|
+
text(item.ref) &&
|
|
106
|
+
path.isAbsolute(item.ref) &&
|
|
107
|
+
digest(item.digest), 'capabilities.evidence_records item');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const kind of ['skills', 'tools']) {
|
|
111
|
+
assert(Array.isArray(card[kind]), `capabilities.${kind}`);
|
|
112
|
+
const ids = new Set();
|
|
113
|
+
for (const item of card[kind]) {
|
|
114
|
+
assert(record(item) && text(item.id) && states.has(item.state), `capabilities.${kind} item`);
|
|
115
|
+
assert(!ids.has(item.id), `capabilities.${kind} duplicate`);
|
|
116
|
+
ids.add(item.id);
|
|
117
|
+
if (kind === 'skills') {
|
|
118
|
+
assert(text(item.path) &&
|
|
119
|
+
path.isAbsolute(item.path) &&
|
|
120
|
+
digest(item.bundle_digest), 'capabilities.skills fingerprint');
|
|
121
|
+
}
|
|
122
|
+
if (readyStates.has(item.state))
|
|
123
|
+
assert(text(item.evidence_ref), `capabilities.${kind} evidence_ref`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Validate reported readiness, not authorization, resource locking, or task completion. */
|
|
128
|
+
export function preflight(contract, card, { now = Date.now(), maxAgeMinutes = 30 } = {}) {
|
|
129
|
+
try {
|
|
130
|
+
assert(Number.isFinite(now) &&
|
|
131
|
+
Number.isFinite(new Date(now).getTime()) &&
|
|
132
|
+
Number.isFinite(maxAgeMinutes) &&
|
|
133
|
+
maxAgeMinutes > 0, 'clock/maximum age');
|
|
134
|
+
validateContractAndCard(contract, card);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
status: 'invalid',
|
|
139
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
140
|
+
blockers: [],
|
|
141
|
+
warnings: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const blockers = [];
|
|
145
|
+
const warnings = [];
|
|
146
|
+
const add = (list, code, capability) => list.push({ code, ...(capability ? { capability } : {}) });
|
|
147
|
+
for (const key of [
|
|
148
|
+
'agent_id',
|
|
149
|
+
'session_id',
|
|
150
|
+
'harness',
|
|
151
|
+
'surface',
|
|
152
|
+
'configuration_id',
|
|
153
|
+
]) {
|
|
154
|
+
if (contract.assignee[key] !== card[key])
|
|
155
|
+
add(blockers, `assignee_${key}_mismatch`);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
if (!fs.statSync(card.cwd).isDirectory() ||
|
|
159
|
+
fs.realpathSync(contract.assignee.cwd) !== fs.realpathSync(card.cwd)) {
|
|
160
|
+
add(blockers, 'assignee_cwd_mismatch');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
add(blockers, 'assignee_cwd_unavailable');
|
|
165
|
+
}
|
|
166
|
+
const age = now - Date.parse(card.observed_at);
|
|
167
|
+
if (age > maxAgeMinutes * 60_000)
|
|
168
|
+
add(blockers, 'capabilities_stale');
|
|
169
|
+
if (age < -60_000)
|
|
170
|
+
add(blockers, 'capabilities_from_future');
|
|
171
|
+
if (card.evidence_level !== 'runtime')
|
|
172
|
+
add(blockers, 'runtime_capabilities_unconfirmed');
|
|
173
|
+
const omitted = contract.runner.omitted_capabilities;
|
|
174
|
+
if ((omitted.includes('skills') && contract.required.skills.length) ||
|
|
175
|
+
(omitted.includes('tools') && contract.required.tools.length) ||
|
|
176
|
+
contract.required.skills.some((skill) => omitted.includes(`skill:${skill.id}`)) ||
|
|
177
|
+
contract.required.tools.some((tool) => omitted.includes(`tool:${tool}`)) ||
|
|
178
|
+
(omitted.includes('mcp') &&
|
|
179
|
+
contract.required.tools.some((tool) => /^(mcp:|mcp__)/.test(tool)))) {
|
|
180
|
+
add(blockers, 'required_capability_omitted_by_runner');
|
|
181
|
+
}
|
|
182
|
+
function evidenceAvailable(item) {
|
|
183
|
+
if (item.evidence_ref === `session:${card.session_id}`)
|
|
184
|
+
return true;
|
|
185
|
+
if (!text(item.evidence_ref) || !path.isAbsolute(item.evidence_ref))
|
|
186
|
+
return false;
|
|
187
|
+
try {
|
|
188
|
+
return fs.statSync(item.evidence_ref).isFile();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function check(group, target) {
|
|
195
|
+
for (const kind of ['skills', 'tools']) {
|
|
196
|
+
for (const required of kind === 'skills'
|
|
197
|
+
? group.skills
|
|
198
|
+
: group.tools.map((id) => ({ id, bundle_digest: null }))) {
|
|
199
|
+
const id = required.id;
|
|
200
|
+
const item = card[kind].find((candidate) => candidate.id === id);
|
|
201
|
+
if (!item || !readyStates.has(item.state)) {
|
|
202
|
+
add(target, `${kind}_unavailable`, id);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!evidenceAvailable(item))
|
|
206
|
+
add(target, `${kind}_evidence_unavailable`, id);
|
|
207
|
+
if (kind === 'skills') {
|
|
208
|
+
if (item.bundle_digest !== required.bundle_digest)
|
|
209
|
+
add(target, 'skill_version_mismatch', id);
|
|
210
|
+
try {
|
|
211
|
+
if (fingerprintSkill(item.path ?? '').bundle_digest !==
|
|
212
|
+
item.bundle_digest)
|
|
213
|
+
add(target, 'skill_files_changed', id);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
add(target, 'skill_files_unavailable', id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
check(contract.required, blockers);
|
|
223
|
+
check(contract.preferred, warnings);
|
|
224
|
+
for (const evidence of card.evidence_records ?? []) {
|
|
225
|
+
try {
|
|
226
|
+
if (!fs.statSync(evidence.ref).isFile() ||
|
|
227
|
+
fs.statSync(evidence.ref).size > 16 * 1024 * 1024)
|
|
228
|
+
throw new Error('Evidence limit');
|
|
229
|
+
const actual = `sha256:${createHash('sha256').update(fs.readFileSync(evidence.ref)).digest('hex')}`;
|
|
230
|
+
if (actual !== evidence.digest)
|
|
231
|
+
add(blockers, 'native_evidence_changed');
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
add(blockers, 'native_evidence_unavailable');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const input of contract.inputs) {
|
|
238
|
+
try {
|
|
239
|
+
const stat = fs.statSync(input.ref);
|
|
240
|
+
if (!stat.isFile() || stat.size > 16 * 1024 * 1024)
|
|
241
|
+
throw new Error('Input limit');
|
|
242
|
+
// Read bytes only to hash them; never include the input text in diagnostics.
|
|
243
|
+
const actual = `sha256:${createHash('sha256').update(fs.readFileSync(input.ref)).digest('hex')}`;
|
|
244
|
+
if (actual !== input.digest)
|
|
245
|
+
add(blockers, 'input_changed');
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
add(blockers, 'input_unavailable');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (contract.runner.omitted_capabilities.length)
|
|
252
|
+
add(warnings, 'runner_capabilities_omitted');
|
|
253
|
+
if (contract.resources.some((resource) => resource.access === 'write'))
|
|
254
|
+
add(warnings, 'resource_ownership_not_enforced');
|
|
255
|
+
return {
|
|
256
|
+
status: blockers.length ? 'needs_input' : 'ready',
|
|
257
|
+
task_id: contract.task_id,
|
|
258
|
+
assignment_id: contract.assignment_id,
|
|
259
|
+
attempt: contract.attempt,
|
|
260
|
+
checked_at: new Date(now).toISOString(),
|
|
261
|
+
errors: [],
|
|
262
|
+
blockers,
|
|
263
|
+
warnings,
|
|
264
|
+
limits: [
|
|
265
|
+
'reported_runtime_evidence_only',
|
|
266
|
+
'authorization_not_checked',
|
|
267
|
+
'no_resource_lock',
|
|
268
|
+
'no_dispatch',
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { TaskRecord, Identity, Resource, ResourceInput, Claim, UnreadableRecord } from './types.js';
|
|
2
|
+
export declare const DOMAIN_LOCK = ".domain.lock";
|
|
3
|
+
export declare class DomainLockHeldError extends Error {
|
|
4
|
+
readonly holder: unknown;
|
|
5
|
+
constructor(message: string, holder: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare function acquireDomainLock(domainRoot: string, { waitMs, pollMs, holder, }?: {
|
|
8
|
+
waitMs?: number;
|
|
9
|
+
pollMs?: number;
|
|
10
|
+
holder?: Record<string, unknown>;
|
|
11
|
+
}): {
|
|
12
|
+
schema: string;
|
|
13
|
+
scope: string;
|
|
14
|
+
domain_root: string;
|
|
15
|
+
nonce: string;
|
|
16
|
+
pid: number;
|
|
17
|
+
at: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function releaseDomainLock(domainRoot: string, holder: {
|
|
20
|
+
nonce: string;
|
|
21
|
+
}): void;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the git index/HEAD resources implied by a path inside a work tree.
|
|
24
|
+
* Only per-worktree index/HEAD are derived automatically; a shared common-dir
|
|
25
|
+
* operation must be claimed explicitly so ordinary edits in two worktrees stay parallel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function gitResourcesFor(targetPath: string): ResourceInput[];
|
|
28
|
+
export declare function expandResources(requested: ResourceInput[], { includeGit, includeSharedGit }?: {
|
|
29
|
+
includeGit?: boolean | undefined;
|
|
30
|
+
includeSharedGit?: boolean | undefined;
|
|
31
|
+
}): Resource[];
|
|
32
|
+
export declare function listDomainRecords(domainRoot: string): (TaskRecord | UnreadableRecord)[];
|
|
33
|
+
export declare function activeClaims(record: TaskRecord | UnreadableRecord): Claim[];
|
|
34
|
+
export declare function scanConflicts(records: (TaskRecord | UnreadableRecord)[], candidates: Resource[], { access, holder }: {
|
|
35
|
+
access: string;
|
|
36
|
+
holder: Identity;
|
|
37
|
+
}): ({
|
|
38
|
+
resource_key: string;
|
|
39
|
+
kind: string;
|
|
40
|
+
held_by: null;
|
|
41
|
+
held_task: string;
|
|
42
|
+
held_claim: null;
|
|
43
|
+
held_access: string;
|
|
44
|
+
relation: string;
|
|
45
|
+
} | {
|
|
46
|
+
resource_key: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
held_by: Identity;
|
|
49
|
+
held_task: string;
|
|
50
|
+
held_claim: string;
|
|
51
|
+
held_access: string;
|
|
52
|
+
relation: string;
|
|
53
|
+
})[];
|
|
54
|
+
/**
|
|
55
|
+
* Claim resources for one task. The domain lock serialises the decision so two tasks
|
|
56
|
+
* cannot both observe an empty domain and then claim the same path.
|
|
57
|
+
*/
|
|
58
|
+
export declare function claimResources(taskRoot: string, { resources, access, holder, waitMs, clock, includeSharedGit, note, subtaskId, attempt, }: {
|
|
59
|
+
resources: ResourceInput[];
|
|
60
|
+
access?: string;
|
|
61
|
+
holder: Identity;
|
|
62
|
+
waitMs?: number;
|
|
63
|
+
clock?: number;
|
|
64
|
+
includeSharedGit?: boolean;
|
|
65
|
+
note?: string | null;
|
|
66
|
+
subtaskId?: string | null;
|
|
67
|
+
attempt?: number | null;
|
|
68
|
+
}): {
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
ok: false;
|
|
71
|
+
reason: string;
|
|
72
|
+
} | {
|
|
73
|
+
ok: true;
|
|
74
|
+
claims: Claim[];
|
|
75
|
+
resource_epoch: number;
|
|
76
|
+
conflicts: never[];
|
|
77
|
+
};
|
|
78
|
+
export declare function releaseClaim(taskRoot: string, { claimId, holder, reason, clock, }: {
|
|
79
|
+
claimId: string;
|
|
80
|
+
holder?: Identity;
|
|
81
|
+
reason?: string;
|
|
82
|
+
clock?: number;
|
|
83
|
+
}): {
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
ok: false;
|
|
86
|
+
reason: string;
|
|
87
|
+
} | {
|
|
88
|
+
ok: true;
|
|
89
|
+
claim_id: string;
|
|
90
|
+
};
|
|
91
|
+
export declare function domainClaims(record: TaskRecord): {
|
|
92
|
+
task_id: string;
|
|
93
|
+
claims: {
|
|
94
|
+
kind: string;
|
|
95
|
+
scope: string;
|
|
96
|
+
key: string;
|
|
97
|
+
path?: string | null;
|
|
98
|
+
port?: number | null;
|
|
99
|
+
existing?: boolean;
|
|
100
|
+
label: string;
|
|
101
|
+
origin?: string;
|
|
102
|
+
derived_from?: string | null;
|
|
103
|
+
claim_id: string;
|
|
104
|
+
access: string;
|
|
105
|
+
holder: Identity;
|
|
106
|
+
resource_epoch: number;
|
|
107
|
+
claimed_at: string;
|
|
108
|
+
released_at: string | null;
|
|
109
|
+
subtask_id: string | null;
|
|
110
|
+
attempt: number | null;
|
|
111
|
+
note: string | null;
|
|
112
|
+
cancel_pending?: boolean;
|
|
113
|
+
released_by?: string;
|
|
114
|
+
release_reason?: string | null;
|
|
115
|
+
transferred_from?: Identity;
|
|
116
|
+
transferred_at?: string;
|
|
117
|
+
task_id: string;
|
|
118
|
+
}[];
|
|
119
|
+
}[];
|