@kingsoftcloud/agentkit-harness-node 0.0.0-stage → 0.0.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/LICENSE +201 -0
- package/NOTICE +1 -0
- package/README.md +124 -2
- package/THIRD_PARTY_NOTICES.md +66 -0
- package/ZCode-LICENSE +202 -0
- package/dist/index.mjs +4871 -0
- package/dist/native-command-plan.LICENSE.txt +221 -0
- package/dist/native-command-plan.json +16 -0
- package/dist/native-command-plan.mjs +8 -0
- package/dist/rpc-entry.mjs +5682 -0
- package/package.json +169 -4
- package/src/native/a2a-receipt-source.mjs +19 -0
- package/src/native/a2a-tools.mjs +517 -0
- package/src/native/activity-status.mjs +30 -0
- package/src/native/artifact-codec.mjs +481 -0
- package/src/native/artifact-store.mjs +1162 -0
- package/src/native/assistant-stream.mjs +76 -0
- package/src/native/background-command-port.mjs +95 -0
- package/src/native/browser-action-observation.mjs +43 -0
- package/src/native/browser-availability.mjs +22 -0
- package/src/native/browser-canvas-metrics.mjs +71 -0
- package/src/native/browser-dom.mjs +193 -0
- package/src/native/browser-playwright.mjs +1249 -0
- package/src/native/browser-vision.mjs +369 -0
- package/src/native/browser-workspace-policy.mjs +22 -0
- package/src/native/browser-workspace-protocol.mjs +95 -0
- package/src/native/browser-workspace-server.mjs +164 -0
- package/src/native/browser-workspace.mjs +355 -0
- package/src/native/captured-source.mjs +3 -0
- package/src/native/child-budget-allocation.mjs +87 -0
- package/src/native/child-capabilities.mjs +35 -0
- package/src/native/child-command-resource.mjs +20 -0
- package/src/native/child-history.mjs +188 -0
- package/src/native/child-merge.mjs +98 -0
- package/src/native/child-model-surface.mjs +19 -0
- package/src/native/child-observation.mjs +100 -0
- package/src/native/child-owner.mjs +620 -0
- package/src/native/child-publication.mjs +76 -0
- package/src/native/child-readers.mjs +366 -0
- package/src/native/child-tools.mjs +46 -0
- package/src/native/child-workspace-binding.mjs +29 -0
- package/src/native/child-workspace-tools.mjs +1 -0
- package/src/native/child-workspaces.mjs +655 -0
- package/src/native/children.mjs +365 -0
- package/src/native/command-plan-worker.mjs +14 -0
- package/src/native/command-plan.mjs +31 -0
- package/src/native/command-progress.mjs +64 -0
- package/src/native/command-shell.mjs +9 -0
- package/src/native/command-workspace-observation.mjs +36 -0
- package/src/native/component-tools.mjs +280 -0
- package/src/native/context-counter.mjs +133 -0
- package/src/native/context-markers.mjs +51 -0
- package/src/native/context-profile.mjs +72 -0
- package/src/native/context-status.mjs +27 -0
- package/src/native/conversation-command-control.mjs +133 -0
- package/src/native/delivery-timeline.mjs +39 -0
- package/src/native/dsh-background-ownership.mjs +102 -0
- package/src/native/dsh-managed-ownership.mjs +23 -0
- package/src/native/dsh-turn-contract.mjs +3 -0
- package/src/native/dsh-turn-ownership.mjs +54 -0
- package/src/native/dsh-turn-runtime.mjs +498 -0
- package/src/native/e2b-background-supervisor.mjs +347 -0
- package/src/native/e2b-background.mjs +368 -0
- package/src/native/e2b-command-state.mjs +40 -0
- package/src/native/e2b-command-tools.mjs +486 -0
- package/src/native/e2b-sandbox-owner.mjs +227 -0
- package/src/native/hook-executor.mjs +249 -0
- package/src/native/instruction-assembly.mjs +60 -0
- package/src/native/instruction-context.mjs +54 -0
- package/src/native/local-background-supervisor.mjs +108 -0
- package/src/native/local-background.mjs +207 -0
- package/src/native/local-command-evidence.mjs +57 -0
- package/src/native/local-command-policy-worker.mjs +197 -0
- package/src/native/local-command-policy.mjs +46 -0
- package/src/native/local-command-state.mjs +177 -0
- package/src/native/local-command-tools.mjs +302 -0
- package/src/native/local-execution-profile.mjs +36 -0
- package/src/native/local-output-policy.mjs +92 -0
- package/src/native/local-process-bin/darwin-arm64/launcher +0 -0
- package/src/native/local-process-bin/darwin-x64/launcher +0 -0
- package/src/native/local-process-bin/linux-arm64/launcher +0 -0
- package/src/native/local-process-bin/linux-x64/launcher +0 -0
- package/src/native/local-process-launcher.c +183 -0
- package/src/native/local-process-licenses/GCC-COPYING.RUNTIME +72 -0
- package/src/native/local-process-licenses/GCC-COPYING3 +674 -0
- package/src/native/local-process-licenses/NOTICE +21 -0
- package/src/native/local-process-licenses/musl-COPYRIGHT +193 -0
- package/src/native/local-process-manifest.json +32 -0
- package/src/native/local-process.mjs +246 -0
- package/src/native/local-resource-profile.mjs +32 -0
- package/src/native/memory-candidate.mjs +121 -0
- package/src/native/memory-extraction-run.mjs +60 -0
- package/src/native/memory-extraction-tools.mjs +116 -0
- package/src/native/memory-recall.mjs +65 -0
- package/src/native/memory-store.mjs +334 -0
- package/src/native/memory-tools.mjs +329 -0
- package/src/native/model-anthropic-cache.mjs +49 -0
- package/src/native/model-anthropic-tool-search.mjs +85 -0
- package/src/native/model-anthropic.mjs +753 -0
- package/src/native/model-chat-usage.mjs +24 -0
- package/src/native/model-inline-reasoning.mjs +111 -0
- package/src/native/model-media.mjs +174 -0
- package/src/native/model-normalize.mjs +228 -0
- package/src/native/model-openai-chat.mjs +561 -0
- package/src/native/model-port.mjs +361 -0
- package/src/native/model-provider-compat.mjs +129 -0
- package/src/native/model-replay.mjs +153 -0
- package/src/native/model-request.mjs +15 -0
- package/src/native/model-responses.mjs +1205 -0
- package/src/native/model-sse.mjs +171 -0
- package/src/native/model-surface-host.mjs +27 -0
- package/src/native/model-tool-search-policy.mjs +16 -0
- package/src/native/model-transport-observation.mjs +27 -0
- package/src/native/owned-child.mjs +114 -0
- package/src/native/owned-command.mjs +106 -0
- package/src/native/python-syntax-worker.mjs +60 -0
- package/src/native/python-syntax.mjs +50 -0
- package/src/native/read-interruption.mjs +16 -0
- package/src/native/result-stream.mjs +63 -0
- package/src/native/ruff-ast.mjs +95 -0
- package/src/native/run-observation.mjs +128 -0
- package/src/native/session-delivery-journal.mjs +96 -0
- package/src/native/session-delivery.mjs +434 -0
- package/src/native/shell-analysis.mjs +50 -0
- package/src/native/skill-disclosure.mjs +404 -0
- package/src/native/skill-events.mjs +164 -0
- package/src/native/skill-tools.mjs +813 -0
- package/src/native/source-page.mjs +73 -0
- package/src/native/sqlite-session-inbox.mjs +42 -0
- package/src/native/sqlite-store.mjs +617 -0
- package/src/native/tool-authority.mjs +99 -0
- package/src/native/tool-guidance.mjs +12 -0
- package/src/native/tool-observation.mjs +75 -0
- package/src/native/tool-states.mjs +14 -0
- package/src/native/verification-authority.mjs +37 -0
- package/src/native/web-tools.mjs +643 -0
- package/src/native/workspace-checkpoints.mjs +312 -0
- package/src/native/workspace-context.mjs +61 -0
- package/src/native/workspace-display.mjs +39 -0
- package/src/native/workspace-edit-match.mjs +19 -0
- package/src/native/workspace-edit-worker.mjs +15 -0
- package/src/native/workspace-editing.mjs +447 -0
- package/src/native/workspace-image.mjs +72 -0
- package/src/native/workspace-instructions.mjs +107 -0
- package/src/native/workspace-mutation-lock.mjs +74 -0
- package/src/native/workspace-patch-worker.mjs +17 -0
- package/src/native/workspace-patch.mjs +17 -0
- package/src/native/workspace-paths.mjs +225 -0
- package/src/native/workspace-state.mjs +272 -0
- package/src/native/workspace-subject-observer.mjs +84 -0
- package/src/native/workspace-tools.mjs +1740 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// Native Skills progressive disclosure port.
|
|
2
|
+
//
|
|
3
|
+
// Ports ksadk/harness/skill_adapters/native_disclosure.py and
|
|
4
|
+
// ksadk/harness/skill_runtime.py. Four formal tools expose an immutable
|
|
5
|
+
// host-bound Skill allowlist with four disclosure levels.
|
|
6
|
+
//
|
|
7
|
+
// Security invariants (review4):
|
|
8
|
+
// 1. restoreRun validates effect.prepared events in record.events — not
|
|
9
|
+
// just intents+receipts. Each disclosure receipt must have a
|
|
10
|
+
// corresponding prepared event with matching operationKey+kind=tool.
|
|
11
|
+
// Removing events from store.load projection must NOT grant L2/L3.
|
|
12
|
+
// 2. assertCurrent uses real store lease/intent/fence validation, not
|
|
13
|
+
// always-true. Tests use real NativeKernel with unique runIds.
|
|
14
|
+
// 3. scope validated via constructor (trusted), not just 4-field key.
|
|
15
|
+
// 4. Error messages are bounded diagnostics — no Host paths/credentials.
|
|
16
|
+
// 5. approval_required returns top-level {status, interactionId}.
|
|
17
|
+
// 6. All source methods awaited with strict type checks; final guard
|
|
18
|
+
// before return.
|
|
19
|
+
|
|
20
|
+
import { canonical, copy, invariant, scopeKey, toolInputRejection, matchesToolExecutionContract } from '@kingsoftcloud/agentkit-harness-core';
|
|
21
|
+
|
|
22
|
+
export const SKILL_DISCLOSURE_CONTRACT = 'agentkit.native-skill-disclosure/v1';
|
|
23
|
+
export const SKILL_CATALOG_TOOL = 'skill_catalog';
|
|
24
|
+
export const SKILL_MANIFEST_TOOL = 'skill_read_manifest';
|
|
25
|
+
export const SKILL_INSTRUCTIONS_TOOL = 'skill_read_instructions';
|
|
26
|
+
export const SKILL_RESOURCE_TOOL = 'skill_read_resource';
|
|
27
|
+
const RESOURCE_MAX_BYTES = 32768;
|
|
28
|
+
|
|
29
|
+
function safeMessage(code) { return { REQUEST_CANCELLED: 'Cancelled', SKILL_AUTHORIZATION_EXPIRED: 'Authorization expired', NATIVE_SKILL_DRIFT: 'Request drifted', NATIVE_SKILL_SIGNAL_DRIFT: 'Signal replaced', NATIVE_SKILL_DEFINITION_FORGED: 'Definition mismatch', NATIVE_SKILL_DEFINITION_MISSING: 'Definition required', NATIVE_SKILL_CALL_MISSING: 'Call missing', NATIVE_SKILL_CALL_ID_MISSING: 'CallId required', NATIVE_SKILL_IDENTITY_MISSING: 'Identity missing', NATIVE_SKILL_IDENTITY_INVALID: 'Identity invalid', NATIVE_SKILL_AUTHORITY_SCOPE_MISMATCH: 'Scope mismatch', NATIVE_SKILL_AUTHORITY_SESSION_MISMATCH: 'Session mismatch', skill_replay_unavailable: 'Replay unavailable', skill_replay_scope_mismatch: 'Scope mismatch', skill_replay_session_mismatch: 'Session mismatch', skill_not_bound: 'Skill not bound', invalid_skill_query: 'Invalid query', invalid_skill_resource_ref: 'Invalid resource ref', skill_disclosure_order_invalid: 'Disclosure order invalid', skill_resource_unavailable: 'Resource unavailable', skill_resource_too_large: 'Resource too large', skill_source_unavailable: 'Source unavailable' }[code] || code; }
|
|
30
|
+
|
|
31
|
+
function searchTerms(value) {
|
|
32
|
+
const normalized = (value || '').toLowerCase();
|
|
33
|
+
const terms = new Set();
|
|
34
|
+
for (const token of normalized.matchAll(/[a-z0-9][a-z0-9._-]*/g)) {
|
|
35
|
+
const t = token[0].replace(/^[._-]+|[._-]+$/g, '');
|
|
36
|
+
if (t) terms.add(t);
|
|
37
|
+
for (const part of t.split(/[._-]+/)) if (part) terms.add(part);
|
|
38
|
+
}
|
|
39
|
+
for (const chunk of normalized.matchAll(/[㐀-鿿]+/g)) {
|
|
40
|
+
const c = chunk[0];
|
|
41
|
+
if (c.length === 1) terms.add(c);
|
|
42
|
+
else for (let i = 0; i < c.length - 1; i++) terms.add(c.slice(i, i + 2));
|
|
43
|
+
}
|
|
44
|
+
return terms;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const skillIdSchema = { type: 'string', description: 'Revision-bound Skill reference.' };
|
|
48
|
+
const DEFINITIONS = [
|
|
49
|
+
{ name: SKILL_CATALOG_TOOL, description: 'List the Skills bound to this Agent revision. Unlisted Skills are unavailable.',
|
|
50
|
+
parameters: { type: 'object', properties: { query: { type: 'string' } }, additionalProperties: false },
|
|
51
|
+
readOnly: true, concurrentSafe: true, permissions: [], timeoutMs: 5000,
|
|
52
|
+
contextRole: 'none', observationRole: 'none', progressObservation: false, contextRetention: 'persistent' },
|
|
53
|
+
{ name: SKILL_MANIFEST_TOOL, description: "Read a bound Skill's manifest, conditions and required tools. You can also read full instructions directly.",
|
|
54
|
+
parameters: { type: 'object', properties: { skill_id: skillIdSchema }, required: ['skill_id'], additionalProperties: false },
|
|
55
|
+
readOnly: true, concurrentSafe: true, permissions: [], timeoutMs: 10000,
|
|
56
|
+
contextRole: 'none', observationRole: 'none', progressObservation: false, contextRetention: 'persistent' },
|
|
57
|
+
{ name: SKILL_INSTRUCTIONS_TOOL, description: "Read a bound Skill's full instructions. No prior manifest read required.",
|
|
58
|
+
parameters: { type: 'object', properties: { skill_id: skillIdSchema }, required: ['skill_id'], additionalProperties: false },
|
|
59
|
+
readOnly: true, concurrentSafe: true, permissions: [], timeoutMs: 10000,
|
|
60
|
+
contextRole: 'none', observationRole: 'none', progressObservation: false, contextRetention: 'persistent' },
|
|
61
|
+
{ name: SKILL_RESOURCE_TOOL, description: "Read a Skill's referenced resource. Requires prior instructions read for the same Skill in the same Run.",
|
|
62
|
+
parameters: { type: 'object', properties: { skill_id: skillIdSchema, resource_ref: { type: 'string', description: 'Package-relative resource path.' } }, required: ['skill_id', 'resource_ref'], additionalProperties: false },
|
|
63
|
+
readOnly: true, concurrentSafe: true, permissions: [], timeoutMs: 10000,
|
|
64
|
+
contextRole: 'none', observationRole: 'none', progressObservation: false, contextRetention: 'persistent' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
function validateRequestIdentity(request) {
|
|
68
|
+
const { scope, runId, sessionId, turnId, buildId, parentRunId, taskChainId, operationKey, executionFence, signal } = request;
|
|
69
|
+
if (!(scope && typeof scope === 'object' && !Array.isArray(scope))) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
70
|
+
try { scopeKey(scope); } catch { return { code: 'NATIVE_SKILL_IDENTITY_INVALID' }; }
|
|
71
|
+
if (typeof runId !== 'string' || !runId.trim()) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
72
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
73
|
+
if (typeof turnId !== 'string' || !turnId.trim()) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
74
|
+
if (typeof buildId !== 'string' || !buildId.trim()) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
75
|
+
if (parentRunId !== null && (typeof parentRunId !== 'string' || !parentRunId.trim())) return { code: 'NATIVE_SKILL_IDENTITY_INVALID' };
|
|
76
|
+
if (typeof taskChainId !== 'string' || !taskChainId.trim()) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
77
|
+
if (typeof operationKey !== 'string' || !operationKey.trim()) return { code: 'NATIVE_SKILL_IDENTITY_MISSING' };
|
|
78
|
+
if (!Number.isSafeInteger(executionFence) || executionFence <= 0) return { code: 'NATIVE_SKILL_IDENTITY_INVALID' };
|
|
79
|
+
if (!(signal instanceof AbortSignal)) return { code: 'NATIVE_SKILL_IDENTITY_INVALID' };
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function snapshotRequest(request, internalDef) {
|
|
84
|
+
const { signal, ...rest } = request;
|
|
85
|
+
return canonical({ ...rest, internalDefinition: internalDef });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ALLOWED_ARGS = {
|
|
89
|
+
[SKILL_CATALOG_TOOL]: new Set(['query']),
|
|
90
|
+
[SKILL_MANIFEST_TOOL]: new Set(['skill_id']),
|
|
91
|
+
[SKILL_INSTRUCTIONS_TOOL]: new Set(['skill_id']),
|
|
92
|
+
[SKILL_RESOURCE_TOOL]: new Set(['skill_id', 'resource_ref']),
|
|
93
|
+
};
|
|
94
|
+
const REQUIRED_ARGS = {
|
|
95
|
+
[SKILL_CATALOG_TOOL]: [],
|
|
96
|
+
[SKILL_MANIFEST_TOOL]: ['skill_id'],
|
|
97
|
+
[SKILL_INSTRUCTIONS_TOOL]: ['skill_id'],
|
|
98
|
+
[SKILL_RESOURCE_TOOL]: ['skill_id', 'resource_ref'],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
class SkillRuntime {
|
|
102
|
+
constructor(source) { this._source = source; }
|
|
103
|
+
level(levels, runId, skillId) { return levels.get(`${runId}:${skillId}`) ?? 0; }
|
|
104
|
+
restoreLevel(levels, runId, skillId, level) {
|
|
105
|
+
const current = this.level(levels, runId, skillId);
|
|
106
|
+
if (![1, 2, 3].includes(level) || (level === 3 && current < 2))
|
|
107
|
+
throw Object.assign(new Error('order invalid'), { code: 'SkillDisclosureError' });
|
|
108
|
+
levels.set(`${runId}:${skillId}`, Math.max(current, level));
|
|
109
|
+
}
|
|
110
|
+
async manifest(skillId) {
|
|
111
|
+
const m = await this._source.manifest(skillId);
|
|
112
|
+
if (!m || typeof m !== 'object') throw new Error('manifest must return object');
|
|
113
|
+
return m;
|
|
114
|
+
}
|
|
115
|
+
async fullText(skillId) {
|
|
116
|
+
const t = await this._source.fullText(skillId);
|
|
117
|
+
if (typeof t !== 'string') throw new Error('fullText must return string');
|
|
118
|
+
return t;
|
|
119
|
+
}
|
|
120
|
+
async resource(skillId, resourceRef) {
|
|
121
|
+
const r = await this._source.resource(skillId, resourceRef);
|
|
122
|
+
if (!(r instanceof Buffer) && !(r instanceof Uint8Array)) throw new Error('resource must return Buffer/Uint8Array');
|
|
123
|
+
return r;
|
|
124
|
+
}
|
|
125
|
+
async catalogEntry(skillId) {
|
|
126
|
+
const manifest = await this.manifest(skillId);
|
|
127
|
+
return { skillId, name: manifest.name, summary: manifest.summary };
|
|
128
|
+
}
|
|
129
|
+
async recommend(skillIds, { query, limit = 5 } = {}, guard) {
|
|
130
|
+
const entries = [];
|
|
131
|
+
for (const id of skillIds) {
|
|
132
|
+
const g = guard.check(); if (g) throw Object.assign(new Error(g.message), { code: g.code });
|
|
133
|
+
entries.push(await this.catalogEntry(id));
|
|
134
|
+
const g2 = guard.check(); if (g2) throw Object.assign(new Error(g2.message), { code: g2.code });
|
|
135
|
+
}
|
|
136
|
+
const queryTerms = searchTerms(query);
|
|
137
|
+
const ranked = entries.map((entry, index) => {
|
|
138
|
+
const nameTerms = searchTerms(entry.name);
|
|
139
|
+
const summaryTerms = searchTerms(entry.summary);
|
|
140
|
+
const nameHits = [...queryTerms].filter(t => nameTerms.has(t)).length;
|
|
141
|
+
const summaryHits = [...queryTerms].filter(t => summaryTerms.has(t)).length;
|
|
142
|
+
return { score: nameHits * 3 + summaryHits, index, entry };
|
|
143
|
+
});
|
|
144
|
+
ranked.sort((a, b) => b.score - a.score || a.index - b.index);
|
|
145
|
+
const positive = new Set(ranked.slice(0, limit).filter(r => r.score >= 3).map(r => r.index));
|
|
146
|
+
return ranked.map(({ score, index, entry }) => ({
|
|
147
|
+
...entry, recommended: positive.has(index) ? 'true' : 'false', recommendationScore: String(score),
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function nativeSkillDisclosureDefinitions() { return copy(DEFINITIONS); }
|
|
153
|
+
|
|
154
|
+
export function createNativeSkillDisclosurePort({ scope, sessionId, store, source, skillRefs, authorize, assertCurrent } = {}) {
|
|
155
|
+
invariant(typeof authorize === 'function', 'native_skill_disclosure_authorize_required');
|
|
156
|
+
invariant(source && typeof source.manifest === 'function' && typeof source.fullText === 'function' && typeof source.resource === 'function', 'native_skill_disclosure_source_required');
|
|
157
|
+
invariant(typeof assertCurrent === 'function', 'native_skill_disclosure_assert_current_required');
|
|
158
|
+
invariant(store && typeof store.load === 'function', 'native_skill_disclosure_store_required');
|
|
159
|
+
invariant(typeof sessionId === 'string' && sessionId.trim(), 'native_skill_disclosure_session_invalid');
|
|
160
|
+
if (!Array.isArray(skillRefs) || skillRefs.some(ref => typeof ref !== 'string' || !ref.trim()))
|
|
161
|
+
throw Object.assign(new Error('skill_refs invalid'), { code: 'native_skill_refs_invalid' });
|
|
162
|
+
if (new Set(skillRefs).size !== skillRefs.length)
|
|
163
|
+
throw Object.assign(new Error('skill_refs not unique'), { code: 'native_skill_refs_invalid' });
|
|
164
|
+
// Validate trusted constructor scope (not just 4-field key).
|
|
165
|
+
invariant(scope && typeof scope === 'object' && !Array.isArray(scope) && ['tenantId', 'workspaceId', 'userId', 'agentId'].every(k => typeof scope[k] === 'string' && scope[k].trim()), 'native_skill_disclosure_scope_invalid');
|
|
166
|
+
const sealedSkillRefs = copy(skillRefs);
|
|
167
|
+
const sealedSkillRefSet = new Set(sealedSkillRefs);
|
|
168
|
+
const sealedScope = copy(scope);
|
|
169
|
+
const sealedSessionId = sessionId;
|
|
170
|
+
const internalDefinitions = copy(DEFINITIONS);
|
|
171
|
+
const internalDefinitionMap = new Map(internalDefinitions.map(d => [d.name, copy(d)]));
|
|
172
|
+
const publicDefinitions = copy(DEFINITIONS);
|
|
173
|
+
const runtime = new SkillRuntime(source);
|
|
174
|
+
let closed = false;
|
|
175
|
+
|
|
176
|
+
function makeGuard(request, internalDef) {
|
|
177
|
+
const sealed = snapshotRequest(request, internalDef);
|
|
178
|
+
const originalSignal = request.signal;
|
|
179
|
+
const originalRequest = request;
|
|
180
|
+
return {
|
|
181
|
+
check() {
|
|
182
|
+
if (closed) return { code: 'SKILL_AUTHORIZATION_EXPIRED' };
|
|
183
|
+
if (originalSignal.aborted) return { code: 'REQUEST_CANCELLED' };
|
|
184
|
+
if (originalRequest.signal !== originalSignal) return { code: 'NATIVE_SKILL_SIGNAL_DRIFT' };
|
|
185
|
+
let acResult;
|
|
186
|
+
try { acResult = assertCurrent(originalRequest); }
|
|
187
|
+
catch (e) { return { code: 'SKILL_AUTHORIZATION_EXPIRED' }; }
|
|
188
|
+
if (acResult !== true) {
|
|
189
|
+
if (acResult && typeof acResult.then === 'function') { acResult.then(() => {}, () => {}); return { code: 'SKILL_AUTHORIZATION_EXPIRED' }; }
|
|
190
|
+
return { code: 'SKILL_AUTHORIZATION_EXPIRED' };
|
|
191
|
+
}
|
|
192
|
+
if (closed) return { code: 'SKILL_AUTHORIZATION_EXPIRED' };
|
|
193
|
+
if (originalRequest.signal !== originalSignal) return { code: 'NATIVE_SKILL_SIGNAL_DRIFT' };
|
|
194
|
+
if (originalSignal.aborted) return { code: 'REQUEST_CANCELLED' };
|
|
195
|
+
const current = snapshotRequest(originalRequest, internalDef);
|
|
196
|
+
if (current !== sealed) return { code: 'NATIVE_SKILL_DRIFT' };
|
|
197
|
+
return null;
|
|
198
|
+
},
|
|
199
|
+
signal: originalSignal,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Rebuild levels from committed receipts on EVERY request.
|
|
204
|
+
// review4 #1: Must validate effect.prepared events in record.events,
|
|
205
|
+
// not just intents+receipts. Without a matching prepared event, no grant.
|
|
206
|
+
function restoreRun(runId) {
|
|
207
|
+
const levels = new Map();
|
|
208
|
+
let record;
|
|
209
|
+
try { record = store.load(sealedScope, runId); }
|
|
210
|
+
catch (error) { if (error?.code === 'run_not_found') return levels; throw error; }
|
|
211
|
+
if (canonical(record.snapshot.scope) !== canonical(sealedScope))
|
|
212
|
+
throw Object.assign(new Error('scope mismatch'), { code: 'skill_replay_scope_mismatch' });
|
|
213
|
+
if (record.snapshot.sessionId !== sealedSessionId)
|
|
214
|
+
throw Object.assign(new Error('session mismatch'), { code: 'skill_replay_session_mismatch' });
|
|
215
|
+
// Build a set of operationKeys that have a matching effect.prepared event
|
|
216
|
+
// with kind=tool. This is the authoritative proof that the tool effect
|
|
217
|
+
// was actually prepared in the ledger, not just hand-inserted.
|
|
218
|
+
const preparedToolOps = new Set();
|
|
219
|
+
for (const event of record.events ?? []) {
|
|
220
|
+
if (event.kind === 'effect.prepared' && event.payload?.kind === 'tool') {
|
|
221
|
+
preparedToolOps.add(event.payload.operationKey);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Build a map of operationKey → intent for tool intents.
|
|
225
|
+
const toolIntents = new Map();
|
|
226
|
+
for (const intent of record.intents) {
|
|
227
|
+
if (intent.kind !== 'tool') continue;
|
|
228
|
+
toolIntents.set(intent.operationKey, intent);
|
|
229
|
+
}
|
|
230
|
+
// Check receipts that correspond to tool intents WITH a prepared event.
|
|
231
|
+
for (const receipt of record.receipts) {
|
|
232
|
+
if (receipt.status !== 'committed') continue;
|
|
233
|
+
// review4 #1: Must have a matching effect.prepared event.
|
|
234
|
+
if (!preparedToolOps.has(receipt.operationKey)) continue;
|
|
235
|
+
const intent = toolIntents.get(receipt.operationKey);
|
|
236
|
+
if (!intent) continue;
|
|
237
|
+
const calls = intent.payload?.calls;
|
|
238
|
+
const results = receipt.result?.results;
|
|
239
|
+
if (!Array.isArray(calls) || !Array.isArray(results) || calls.length !== results.length) continue;
|
|
240
|
+
for (let i = 0; i < calls.length; i++) {
|
|
241
|
+
const call = calls[i];
|
|
242
|
+
const result = results[i];
|
|
243
|
+
if (result?.status !== 'ok' || typeof call !== 'object' || call === null) continue;
|
|
244
|
+
const args = call.arguments;
|
|
245
|
+
const content = result.content;
|
|
246
|
+
if (typeof args !== 'object' || args === null || typeof content !== 'object' || content === null) continue;
|
|
247
|
+
const allowed = ALLOWED_ARGS[call.name];
|
|
248
|
+
if (!allowed) continue;
|
|
249
|
+
const argKeys = Object.keys(args);
|
|
250
|
+
if (argKeys.some(k => !allowed.has(k))) continue;
|
|
251
|
+
const required = REQUIRED_ARGS[call.name];
|
|
252
|
+
if (required.some(r => !(r in args))) continue;
|
|
253
|
+
const skillId = args.skill_id;
|
|
254
|
+
if (typeof skillId !== 'string' || !sealedSkillRefSet.has(skillId) || content.skill_id !== skillId) continue;
|
|
255
|
+
const levelMap = { [SKILL_MANIFEST_TOOL]: 1, [SKILL_INSTRUCTIONS_TOOL]: 2, [SKILL_RESOURCE_TOOL]: 3 };
|
|
256
|
+
const level = levelMap[call.name];
|
|
257
|
+
if (level === undefined) continue;
|
|
258
|
+
// Validate the formal definition in configuration matches internal.
|
|
259
|
+
const configTools = record.configuration?.tools ?? [];
|
|
260
|
+
const configDef = configTools.find(t => t.name === call.name);
|
|
261
|
+
if (!configDef || !matchesToolExecutionContract(configDef, internalDefinitionMap.get(call.name))) continue;
|
|
262
|
+
try { runtime.restoreLevel(levels, runId, skillId, level); } catch { /* contradictory → deny */ }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return levels;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- async handlers with per-await guards ---
|
|
269
|
+
async function catalogHandler(args, ctx) {
|
|
270
|
+
const { query = '' } = args;
|
|
271
|
+
if (typeof query !== 'string') return { status: 'error', code: 'invalid_skill_query' };
|
|
272
|
+
let g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
273
|
+
try {
|
|
274
|
+
const bound = await runtime.recommend(sealedSkillRefs, { query }, ctx.guard);
|
|
275
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
276
|
+
return { status: 'ok', content: { bound_skills: bound, unlisted_skills_available: false } };
|
|
277
|
+
} catch (e) {
|
|
278
|
+
if (['REQUEST_CANCELLED', 'SKILL_AUTHORIZATION_EXPIRED', 'NATIVE_SKILL_DRIFT', 'NATIVE_SKILL_SIGNAL_DRIFT'].includes(e.code)) return { status: 'error', code: e.code, message: safeMessage(e.code) };
|
|
279
|
+
return { status: 'error', code: 'skill_source_unavailable' };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function manifestHandler(args, ctx) {
|
|
284
|
+
const { skill_id: skillId } = args;
|
|
285
|
+
if (typeof skillId !== 'string' || !sealedSkillRefSet.has(skillId)) return { status: 'error', code: 'skill_not_bound' };
|
|
286
|
+
let g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
287
|
+
try {
|
|
288
|
+
const manifest = await runtime.manifest(skillId);
|
|
289
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
290
|
+
return { status: 'ok', content: { skill_id: skillId, name: manifest.name, summary: manifest.summary, conditions: manifest.conditions || '', required_tools: [...(manifest.requiredTools || [])], resource_base: 'skill-root', resource_access: manifest.resourceRefs != null ? 'locked_package' : 'instructions_only' } };
|
|
291
|
+
} catch { return { status: 'error', code: 'skill_source_unavailable' }; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function instructionsHandler(args, ctx) {
|
|
295
|
+
const { skill_id: skillId } = args;
|
|
296
|
+
if (typeof skillId !== 'string' || !sealedSkillRefSet.has(skillId)) return { status: 'error', code: 'skill_not_bound' };
|
|
297
|
+
let g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
298
|
+
try {
|
|
299
|
+
const instructions = await runtime.fullText(skillId);
|
|
300
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
301
|
+
return { status: 'ok', content: { skill_id: skillId, instructions } };
|
|
302
|
+
} catch (e) {
|
|
303
|
+
if (e.code === 'SkillDisclosureError') return { status: 'error', code: 'skill_disclosure_order_invalid' };
|
|
304
|
+
return { status: 'error', code: 'skill_source_unavailable' };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function resourceHandler(args, ctx) {
|
|
309
|
+
const { skill_id: skillId, resource_ref: resourceRef } = args;
|
|
310
|
+
if (typeof skillId !== 'string' || !sealedSkillRefSet.has(skillId)) return { status: 'error', code: 'skill_not_bound' };
|
|
311
|
+
if (typeof resourceRef !== 'string' || !resourceRef || resourceRef.startsWith('/')
|
|
312
|
+
|| resourceRef.includes('\\') || resourceRef.includes('\0')
|
|
313
|
+
|| resourceRef.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
314
|
+
return { status: 'error', code: 'invalid_skill_resource_ref' };
|
|
315
|
+
}
|
|
316
|
+
if (runtime.level(ctx.levels, ctx.runId, skillId) < 2) return { status: 'error', code: 'skill_disclosure_order_invalid' };
|
|
317
|
+
let g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
318
|
+
try {
|
|
319
|
+
const manifest = await runtime.manifest(skillId);
|
|
320
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
321
|
+
if (manifest.resourceRefs != null) {
|
|
322
|
+
if (!manifest.resourceRefs.includes(resourceRef)) throw Object.assign(new Error('undeclared'), { code: 'SkillDisclosureError' });
|
|
323
|
+
} else {
|
|
324
|
+
const instructions = await runtime.fullText(skillId);
|
|
325
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
326
|
+
if (!instructions.includes(resourceRef)) throw Object.assign(new Error('unreferenced'), { code: 'SkillDisclosureError' });
|
|
327
|
+
}
|
|
328
|
+
const content = await runtime.resource(skillId, resourceRef);
|
|
329
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
330
|
+
if (content.length > RESOURCE_MAX_BYTES) return { status: 'error', code: 'skill_resource_too_large', limit_bytes: RESOURCE_MAX_BYTES };
|
|
331
|
+
g = ctx.guard.check(); if (g) return { status: 'error', code: g.code, message: safeMessage(g.code) };
|
|
332
|
+
try {
|
|
333
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(content);
|
|
334
|
+
return { status: 'ok', content: { skill_id: skillId, resource_ref: resourceRef, encoding: 'utf-8', text } };
|
|
335
|
+
} catch {
|
|
336
|
+
return { status: 'ok', content: { skill_id: skillId, resource_ref: resourceRef, encoding: 'base64', data: Buffer.from(content).toString('base64') } };
|
|
337
|
+
}
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (e.code === 'SkillDisclosureError') return { status: 'error', code: 'skill_resource_unavailable' };
|
|
340
|
+
if (['REQUEST_CANCELLED', 'SKILL_AUTHORIZATION_EXPIRED', 'NATIVE_SKILL_DRIFT', 'NATIVE_SKILL_SIGNAL_DRIFT'].includes(e.code)) return { status: 'error', code: e.code, message: safeMessage(e.code) };
|
|
341
|
+
return { status: 'error', code: 'skill_source_unavailable' };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const handlers = { [SKILL_CATALOG_TOOL]: catalogHandler, [SKILL_MANIFEST_TOOL]: manifestHandler, [SKILL_INSTRUCTIONS_TOOL]: instructionsHandler, [SKILL_RESOURCE_TOOL]: resourceHandler };
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
definitions: publicDefinitions,
|
|
349
|
+
contract: SKILL_DISCLOSURE_CONTRACT,
|
|
350
|
+
close() { closed = true; },
|
|
351
|
+
execute(request = {}) {
|
|
352
|
+
const { call } = request;
|
|
353
|
+
if (!call?.name) return { status: 'error', content: { code: 'NATIVE_SKILL_CALL_MISSING', executed: false, message: safeMessage('NATIVE_SKILL_CALL_MISSING') } };
|
|
354
|
+
const internalDef = internalDefinitionMap.get(call.name);
|
|
355
|
+
if (!internalDef) return { status: 'error', content: { code: 'native_skill_tool_unbound', executed: false, message: 'tool not bound' } };
|
|
356
|
+
if (typeof call.callId !== 'string' || !call.callId.trim()) return { status: 'error', content: { code: 'NATIVE_SKILL_CALL_ID_MISSING', executed: false, message: safeMessage('NATIVE_SKILL_CALL_ID_MISSING') } };
|
|
357
|
+
if (request.definition === undefined) return { status: 'error', content: { code: 'NATIVE_SKILL_DEFINITION_MISSING', executed: false, message: safeMessage('NATIVE_SKILL_DEFINITION_MISSING') } };
|
|
358
|
+
if (!matchesToolExecutionContract(request.definition, internalDef)) return { status: 'error', content: { code: 'NATIVE_SKILL_DEFINITION_FORGED', executed: false, message: safeMessage('NATIVE_SKILL_DEFINITION_FORGED') } };
|
|
359
|
+
const identityError = validateRequestIdentity(request);
|
|
360
|
+
if (identityError) return { status: 'error', content: { ...identityError, executed: false, message: safeMessage(identityError.code) } };
|
|
361
|
+
try { if (scopeKey(request.scope) !== scopeKey(sealedScope)) return { status: 'error', content: { code: 'NATIVE_SKILL_AUTHORITY_SCOPE_MISMATCH', executed: false, message: safeMessage('NATIVE_SKILL_AUTHORITY_SCOPE_MISMATCH') } }; }
|
|
362
|
+
catch { return { status: 'error', content: { code: 'NATIVE_SKILL_IDENTITY_INVALID', executed: false, message: safeMessage('NATIVE_SKILL_IDENTITY_INVALID') } }; }
|
|
363
|
+
if (request.sessionId !== sealedSessionId) return { status: 'error', content: { code: 'NATIVE_SKILL_AUTHORITY_SESSION_MISMATCH', executed: false, message: safeMessage('NATIVE_SKILL_AUTHORITY_SESSION_MISMATCH') } };
|
|
364
|
+
const rejection = toolInputRejection(internalDef, request.call);
|
|
365
|
+
if (rejection) return { status: 'error', content: rejection };
|
|
366
|
+
const guard = makeGuard(request, internalDef);
|
|
367
|
+
if (guard.signal.aborted) return { status: 'error', content: { code: 'REQUEST_CANCELLED', executed: false, message: safeMessage('REQUEST_CANCELLED') } };
|
|
368
|
+
|
|
369
|
+
return (async () => {
|
|
370
|
+
let g0 = guard.check(); if (g0) return { status: 'error', content: { code: g0.code, executed: false, message: safeMessage(g0.code) } };
|
|
371
|
+
let decision;
|
|
372
|
+
try { decision = await authorize(request); }
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (guard.signal.aborted) return { status: 'error', content: { code: 'REQUEST_CANCELLED', executed: false, message: safeMessage('REQUEST_CANCELLED') } };
|
|
375
|
+
return { status: 'error', content: { code: error.code || 'NATIVE_SKILL_ERROR', executed: false, message: 'authorization error' } };
|
|
376
|
+
}
|
|
377
|
+
const gAuth = guard.check();
|
|
378
|
+
if (gAuth) return { status: 'error', content: { code: gAuth.code, executed: false, message: safeMessage(gAuth.code) } };
|
|
379
|
+
if (decision !== 'allow') {
|
|
380
|
+
if (typeof decision === 'object' && (decision.status === 'approval_required' || decision.approval_required))
|
|
381
|
+
return { status: 'approval_required', interactionId: decision.interactionId || '', content: decision };
|
|
382
|
+
const code = typeof decision === 'object' ? decision.code : 'STUDIO_NATIVE_SKILL_PERMISSION_DENIED';
|
|
383
|
+
const message = typeof decision === 'object' ? decision.message : 'not authorized';
|
|
384
|
+
return { status: 'error', content: { code, executed: false, message } };
|
|
385
|
+
}
|
|
386
|
+
const g1 = guard.check(); if (g1) return { status: 'error', content: { code: g1.code, executed: false, message: safeMessage(g1.code) } };
|
|
387
|
+
let levels;
|
|
388
|
+
try { levels = restoreRun(request.runId); }
|
|
389
|
+
catch { return { status: 'error', content: { code: 'skill_replay_unavailable', executed: false, message: safeMessage('skill_replay_unavailable') } }; }
|
|
390
|
+
try {
|
|
391
|
+
guard.signal.throwIfAborted();
|
|
392
|
+
const ctx = { signal: guard.signal, scope: request.scope, runId: request.runId, sessionId: request.sessionId, turnId: request.turnId, buildId: request.buildId, parentRunId: request.parentRunId, taskChainId: request.taskChainId, operationKey: request.operationKey, executionFence: request.executionFence, callId: request.call.callId, guard, levels };
|
|
393
|
+
const result = await handlers[request.call.name](request.call.arguments ?? {}, ctx);
|
|
394
|
+
const gFinal = guard.check(); if (gFinal) return { status: 'error', content: { code: gFinal.code, executed: false, message: safeMessage(gFinal.code) } };
|
|
395
|
+
if (result?.status === 'error') return { status: 'error', content: { ...result, executed: false, message: result.message || safeMessage(result.code) } };
|
|
396
|
+
return { status: 'ok', content: result.content };
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (guard.signal.aborted) return { status: 'error', content: { code: 'REQUEST_CANCELLED', executed: false, message: safeMessage('REQUEST_CANCELLED') } };
|
|
399
|
+
return { status: 'error', content: { code: error.code || 'NATIVE_SKILL_ERROR', executed: false, message: 'execution error' } };
|
|
400
|
+
}
|
|
401
|
+
})();
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** Typed Skill diagnostics. These facts never authorize a tool or attest that
|
|
2
|
+
* an Agent consumed a result. The caller owns persistence and execution fences. */
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { canonical, copy, invariant, parseNativeJson } from '@kingsoftcloud/agentkit-harness-core';
|
|
5
|
+
|
|
6
|
+
export const SKILL_EVENT_SCHEMA_VERSION = 1;
|
|
7
|
+
export const NATIVE_SKILL_EVENTS = 'agentkit.native-skill-events/v1';
|
|
8
|
+
export const SKILL_EVENT_FILE = '.agentkit-skill-events.jsonl';
|
|
9
|
+
export const SKILL_EVENT_BYTES = 256 * 1024;
|
|
10
|
+
const MAX_EVENTS = 1024, MAX_BYTES = 1024 * 1024;
|
|
11
|
+
const TYPES = new Set(['skill.candidates.resolved', 'skill.selection.completed', 'skill.selection.skipped',
|
|
12
|
+
'skill.package.cache_hit', 'skill.package.downloaded', 'skill.package.hash_verified', 'skill.package.extracted',
|
|
13
|
+
'skill.manifest.parsed', 'skill.load.started', 'skill.load.completed', 'skill.load.failed',
|
|
14
|
+
'sandbox.session.created', 'sandbox.session.cleaned_up', 'sandbox.session.cleanup_failed',
|
|
15
|
+
'skill.workflow.started', 'skill.workflow.completed', 'skill.workflow.failed',
|
|
16
|
+
'skill.execution.started', 'skill.execution.completed', 'skill.execution.failed',
|
|
17
|
+
'skill.artifact.created', 'skill.result.consumed', 'sandbox.envelope.rejected']);
|
|
18
|
+
const ATTRIBUTES = new Set(['source', 'scope', 'requested_skill', 'artifact_name', 'artifact_ref', 'agent_step_id',
|
|
19
|
+
'availability', 'backend', 'cache_hit', 'candidate_count', 'candidate_skill_ids', 'has_description', 'mime_type',
|
|
20
|
+
'reason', 'selected_skill_ids', 'selection_receipt_id', 'size_bytes', 'cleanup_scope']);
|
|
21
|
+
const invocationEvent = type => /^(?:skill\.load\.|skill\.execution\.)/.test(type)
|
|
22
|
+
|| type === 'skill.artifact.created' || type === 'skill.result.consumed';
|
|
23
|
+
const validTime = value => Number.isFinite(value) && value >= 0 && value <= 8640000000000;
|
|
24
|
+
const hash = value => createHash('sha256').update(canonical(value)).digest('hex');
|
|
25
|
+
const code = value => typeof value === 'string' && /^[a-z][a-z0-9_]{0,95}$/u.test(value) ? value : '';
|
|
26
|
+
function text(value) {
|
|
27
|
+
if (typeof value !== 'string') return undefined;
|
|
28
|
+
// Attributes are diagnostic labels, never URLs, host paths or credentials.
|
|
29
|
+
if (/https?:\/\/|(?:^|\s)(?:\/|[A-Z]:\\)|bearer\s|(?:api[_-]?key|token|password|secret)\s*[:=]/iu.test(value)) return '[redacted]';
|
|
30
|
+
return Array.from(value).slice(0, 256).join('');
|
|
31
|
+
}
|
|
32
|
+
function attributes(input) {
|
|
33
|
+
const output = {};
|
|
34
|
+
for (const [key, value] of Object.entries(input ?? {})) if (ATTRIBUTES.has(key)) {
|
|
35
|
+
if (typeof value === 'string') output[key] = text(value);
|
|
36
|
+
else if (typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value)) output[key] = value;
|
|
37
|
+
else if (Array.isArray(value)) output[key] = value.slice(0, 100).filter(item => typeof item === 'string').map(text);
|
|
38
|
+
}
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
41
|
+
function refFor(pkg) {
|
|
42
|
+
if (!pkg?.skillId || typeof pkg.versionId !== 'string') return null;
|
|
43
|
+
const checksum = String(pkg.archiveHash || pkg.contentHash || '').toLowerCase().replace(/^sha256:/u, '');
|
|
44
|
+
return { skill_id: String(pkg.skillId), version_id: String(pkg.versionId), version: '', name: String(pkg.name),
|
|
45
|
+
description: '', status: 'active', content_hash: checksum ? `sha256:${checksum}` : '', aliases: [], tags: [] };
|
|
46
|
+
}
|
|
47
|
+
function sameRef(actual, expected) {
|
|
48
|
+
if (expected === null) return actual === null;
|
|
49
|
+
return actual && actual.skill_id === expected.skill_id && actual.version_id === expected.version_id
|
|
50
|
+
&& (!expected.content_hash || actual.content_hash === expected.content_hash);
|
|
51
|
+
}
|
|
52
|
+
export function selectNativeSkillEvents(value) {
|
|
53
|
+
if (!Array.isArray(value) || value.length > MAX_EVENTS || Buffer.byteLength(canonical(value)) > MAX_BYTES) return [];
|
|
54
|
+
return copy(value.filter(event => event?.schema_version === 1 && TYPES.has(event.event_type)
|
|
55
|
+
&& typeof event.event_id === 'string' && /^skill_evt_[a-f0-9]{64}$/u.test(event.event_id)
|
|
56
|
+
&& typeof event.status === 'string' && validTime(event.started_at) && (event.ended_at == null || validTime(event.ended_at) && event.ended_at >= event.started_at)
|
|
57
|
+
&& (!invocationEvent(event.event_type) || typeof event.skill_invocation_id === 'string' && event.skill_invocation_id)));
|
|
58
|
+
}
|
|
59
|
+
export function nativeSkillEventEvidence(events) {
|
|
60
|
+
return Array.isArray(events) ? { skill_event_contract: NATIVE_SKILL_EVENTS, skill_events: selectNativeSkillEvents(events) } : {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Explicit Host API, like Python record_skill_result_consumption. The caller
|
|
64
|
+
* supplies a receipt from its committed Agent step, never from model/sidecar
|
|
65
|
+
* output. Executing a Skill alone deliberately creates no consumption fact. */
|
|
66
|
+
export function recordNativeSkillResultConsumption({ events, receipt, invocationIds }) {
|
|
67
|
+
invariant(receipt?.committed === true && ['id', 'runId', 'runtimeId', 'bindingDigest'].every(key =>
|
|
68
|
+
typeof receipt[key] === 'string' && receipt[key]) && validTime(receipt.at)
|
|
69
|
+
&& Array.isArray(invocationIds) && invocationIds.length <= 100 && new Set(invocationIds).size === invocationIds.length,
|
|
70
|
+
'skill_consumption_receipt_required');
|
|
71
|
+
const facts = selectNativeSkillEvents(events);
|
|
72
|
+
return invocationIds.map(id => {
|
|
73
|
+
const original = facts.find(event => event.skill_invocation_id === id && event.event_type === 'skill.execution.completed'
|
|
74
|
+
&& event.status === 'completed' && event.attributes.source === 'host' && event.run_id === receipt.runId
|
|
75
|
+
&& event.runtime_id === receipt.runtimeId && event.binding_snapshot_id === receipt.bindingDigest);
|
|
76
|
+
return { schema_version: 1, event_id: `skill_evt_${hash([receipt.runtimeId, receipt.id, id, 'consumption'])}`,
|
|
77
|
+
event_type: original ? 'skill.result.consumed' : 'sandbox.envelope.rejected', status: original ? 'completed' : 'rejected',
|
|
78
|
+
started_at: receipt.at, ended_at: receipt.at, skill_ref: original?.skill_ref ?? null,
|
|
79
|
+
skill_invocation_id: original ? id : '', runtime_id: receipt.runtimeId, run_id: receipt.runId,
|
|
80
|
+
trace_id: original?.trace_id ?? '', binding_snapshot_id: receipt.bindingDigest, decision_id: original?.decision_id ?? '',
|
|
81
|
+
attributes: { source: 'host', agent_step_id: text(receipt.id),
|
|
82
|
+
...(receipt.selectionReceiptId ? { selection_receipt_id: text(receipt.selectionReceiptId) } : {}) }, error_code: '',
|
|
83
|
+
error_category: original ? '' : 'invalid_consumption_receipt' };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createNativeSkillEventJournal({ runtimeId, request, bindingDigest, events = [], persist = () => {} }) {
|
|
88
|
+
invariant(typeof runtimeId === 'string' && runtimeId && typeof bindingDigest === 'string' && bindingDigest
|
|
89
|
+
&& Array.isArray(events) && events.length === 0, 'skill_event_journal_invalid');
|
|
90
|
+
const plan = new Map(), seen = new Map(), running = new Map(), starts = new Map(); let bytes = 0, truncated = false;
|
|
91
|
+
const context = { runtime_id: runtimeId, trace_id: String(request.traceId ?? request.runId ?? ''),
|
|
92
|
+
run_id: String(request.runId ?? ''), binding_snapshot_id: bindingDigest,
|
|
93
|
+
decision_id: String(request.operationKey ?? request.callId ?? '') };
|
|
94
|
+
function identity(pkg) {
|
|
95
|
+
const ref = refFor(pkg), key = ref ? [ref.skill_id, ref.version_id] : [pkg.name, hash(pkg.body ?? '')];
|
|
96
|
+
const id = `skill_inv_${hash([runtimeId, bindingDigest, key])}`;
|
|
97
|
+
const previous = plan.get(id);
|
|
98
|
+
invariant(!previous || sameRef(ref, previous.ref) || !previous.ref?.content_hash && sameRef(previous.ref, ref),
|
|
99
|
+
'skill_event_plan_changed');
|
|
100
|
+
plan.set(id, { ref, pkg }); return { skill_ref: copy(ref), skill_invocation_id: id };
|
|
101
|
+
}
|
|
102
|
+
function append(type, status, options = {}) {
|
|
103
|
+
invariant(TYPES.has(type) && typeof status === 'string' && status, 'skill_event_invalid');
|
|
104
|
+
if (truncated) return;
|
|
105
|
+
const correlation = options.pkg ? identity(options.pkg) : { skill_ref: null, skill_invocation_id: '' };
|
|
106
|
+
invariant(!invocationEvent(type) || correlation.skill_invocation_id, 'skill_event_invocation_required');
|
|
107
|
+
const at = options.startedAt ?? Date.now() / 1000;
|
|
108
|
+
const lifecycle = `${type.replace(/\.(?:started|completed|failed|created|cleaned_up|cleanup_failed)$/u, '')}:${correlation.skill_invocation_id || runtimeId}`;
|
|
109
|
+
if (options.source !== 'sandbox' && /\.(?:started|created)$/u.test(type)) starts.set(lifecycle, at);
|
|
110
|
+
const started = options.source !== 'sandbox' && /\.(?:completed|failed|cleaned_up|cleanup_failed)$/u.test(type) ? starts.get(lifecycle) : undefined;
|
|
111
|
+
let event = { schema_version: 1, event_id: `skill_evt_${hash([runtimeId, events.length, type, correlation])}`,
|
|
112
|
+
event_type: type, status, started_at: started ?? at, ended_at: options.endedAt ?? (started === undefined ? null : at), ...correlation, ...context,
|
|
113
|
+
attributes: { ...attributes(options.attributes), source: options.source ?? 'host' },
|
|
114
|
+
error_code: code(options.errorCode ?? ''), error_category: code(options.errorCategory ?? '') };
|
|
115
|
+
let size = Buffer.byteLength(canonical(event));
|
|
116
|
+
if (events.length >= MAX_EVENTS - 1 || bytes + size > MAX_BYTES - 2048) {
|
|
117
|
+
truncated = true;
|
|
118
|
+
event = { ...event, event_type: 'sandbox.envelope.rejected', status: 'rejected', skill_ref: null,
|
|
119
|
+
skill_invocation_id: '', attributes: { reason: 'event_budget_exceeded' }, error_category: 'event_budget_exceeded' };
|
|
120
|
+
size = Buffer.byteLength(canonical(event));
|
|
121
|
+
}
|
|
122
|
+
events.push(copy(event)); bytes += size;
|
|
123
|
+
if (options.source !== 'sandbox' && options.pkg) {
|
|
124
|
+
if (type === 'skill.execution.started') running.set(correlation.skill_invocation_id, options.pkg);
|
|
125
|
+
else if (/^skill\.execution\.(?:completed|failed)$/u.test(type)) running.delete(correlation.skill_invocation_id);
|
|
126
|
+
}
|
|
127
|
+
persist(); return copy(event);
|
|
128
|
+
}
|
|
129
|
+
function reject(reason = 'invalid_envelope') { return append('sandbox.envelope.rejected', 'rejected', { errorCategory: reason }); }
|
|
130
|
+
function accept(raw) {
|
|
131
|
+
if (typeof raw !== 'string' || Buffer.byteLength(raw) > SKILL_EVENT_BYTES) { reject(); return; }
|
|
132
|
+
let lines = 0;
|
|
133
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
134
|
+
if (!line.trim()) continue;
|
|
135
|
+
if (++lines > 512) { reject('event_budget_exceeded'); break; }
|
|
136
|
+
try {
|
|
137
|
+
invariant(Buffer.byteLength(line) <= 16 * 1024, 'envelope_too_large');
|
|
138
|
+
const event = parseNativeJson(line), target = plan.get(event?.skill_invocation_id);
|
|
139
|
+
// Child output cannot claim Host workflow, session, selection or consumption facts.
|
|
140
|
+
invariant(event?.schema_version === 1 && TYPES.has(event.event_type) && target
|
|
141
|
+
&& /^(?:skill\.(?:load|execution|package|manifest|artifact)\.)/u.test(event.event_type)
|
|
142
|
+
&& sameRef(event.skill_ref ?? null, target.ref) && typeof event.event_id === 'string' && event.event_id
|
|
143
|
+
&& event.event_id.length <= 256 && /^[a-z_]{1,32}$/u.test(event.status)
|
|
144
|
+
&& validTime(event.started_at)
|
|
145
|
+
&& (event.ended_at == null || validTime(event.ended_at) && event.ended_at >= event.started_at), 'invalid_envelope');
|
|
146
|
+
const key = hash([event.skill_invocation_id, event.event_id]);
|
|
147
|
+
const fingerprint = hash(event), previous = seen.get(key);
|
|
148
|
+
if (previous) { if (previous !== fingerprint) reject('correlation_mismatch'); continue; }
|
|
149
|
+
seen.set(key, fingerprint);
|
|
150
|
+
append(event.event_type, event.status, { pkg: target.pkg, attributes: event.attributes, source: 'sandbox',
|
|
151
|
+
startedAt: event.started_at, endedAt: event.ended_at, errorCategory: code(event.error_category), errorCode: code(event.error_code) });
|
|
152
|
+
} catch { reject(); }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { emit: append, identity, accept, reject, snapshot: () => copy(events),
|
|
156
|
+
failOpen() { for (const pkg of [...running.values()]) append('skill.execution.failed', 'uncertain', { pkg, errorCategory: 'outcome_unknown' }); },
|
|
157
|
+
commandEnvironment(pkg, file) { return { KSADK_SKILL_EVENT_FILE: file,
|
|
158
|
+
KSADK_SKILL_INVOCATION_ID: identity(pkg).skill_invocation_id, KSADK_SKILL_REF_JSON: canonical(refFor(pkg)) }; },
|
|
159
|
+
artifacts(result, packages) {
|
|
160
|
+
const pkg = packages.find(item => item.name === result.executed_skill);
|
|
161
|
+
if (pkg) for (const item of result.artifact_metadata ?? []) append('skill.artifact.created', 'completed', { pkg,
|
|
162
|
+
attributes: { artifact_ref: `sha256:${item.sha256}`, artifact_name: item.source_path?.split('/').at(-1) ?? '', size_bytes: item.size_bytes } });
|
|
163
|
+
} };
|
|
164
|
+
}
|