@aiwg/cli 2026.7.20 → 2026.7.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -7
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.js +2 -0
- package/dist/src/artifacts/browser-export.js +7 -0
- package/dist/src/artifacts/citation-parser.js +96 -35
- package/dist/src/artifacts/index-builder.js +54 -17
- package/dist/src/artifacts/state-transfer.js +27 -0
- package/dist/src/artifacts/stats.js +8 -0
- package/dist/src/cli/cli-extension-loader.js +73 -0
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/sessions.js +1265 -0
- package/dist/src/cli/handlers/skill-lint.js +49 -45
- package/dist/src/cli/handlers/use.js +143 -60
- package/dist/src/cli/handlers/utilities.js +22 -8
- package/dist/src/cli/skill-usage.js +146 -24
- package/dist/src/config/aiwg-config.js +12 -0
- package/dist/src/config/cli.js +16 -3
- package/dist/src/extensions/commands/definitions.js +29 -0
- package/dist/src/extensions/manifest.js +29 -0
- package/dist/src/security/threat-assessment-config.js +296 -0
- package/dist/src/sessions/adapters/claude.js +385 -0
- package/dist/src/sessions/adapters/codex.js +548 -0
- package/dist/src/sessions/adapters/copilot.js +226 -0
- package/dist/src/sessions/adapters/cursor.js +528 -0
- package/dist/src/sessions/adapters/factory.js +386 -0
- package/dist/src/sessions/adapters/generic.js +225 -0
- package/dist/src/sessions/adapters/hermes.js +341 -0
- package/dist/src/sessions/adapters/openclaw.js +381 -0
- package/dist/src/sessions/adapters/opencode.js +454 -0
- package/dist/src/sessions/adapters/openhuman.js +315 -0
- package/dist/src/sessions/adapters/warp.js +160 -0
- package/dist/src/sessions/adapters/windsurf.js +212 -0
- package/dist/src/sessions/batch-contracts.js +121 -0
- package/dist/src/sessions/batch-import.js +265 -0
- package/dist/src/sessions/candidates.js +210 -0
- package/dist/src/sessions/contracts.js +337 -0
- package/dist/src/sessions/discovery.js +51 -0
- package/dist/src/sessions/fixtures.js +12 -0
- package/dist/src/sessions/import-lease.js +152 -0
- package/dist/src/sessions/importer.js +464 -0
- package/dist/src/sessions/index.js +31 -0
- package/dist/src/sessions/knowledge-shard.js +61 -0
- package/dist/src/sessions/optional-backends.js +238 -0
- package/dist/src/sessions/origin.js +117 -0
- package/dist/src/sessions/policy.js +192 -0
- package/dist/src/sessions/ports.js +2 -0
- package/dist/src/sessions/promotion.js +367 -0
- package/dist/src/sessions/readers.js +176 -0
- package/dist/src/sessions/repository.js +1892 -0
- package/dist/src/sessions/timeline.js +148 -0
- package/dist/src/sessions/workspace-discovery.js +319 -0
- package/dist/src/skills/adapters/agent-skills.js +59 -0
- package/dist/src/skills/adapters/local.js +19 -1
- package/dist/src/skills/agent-skills.js +249 -0
- package/dist/src/skills/cli.js +463 -7
- package/dist/src/skills/deployer.js +554 -0
- package/dist/src/skills/doctor.js +105 -0
- package/dist/src/skills/exporter.js +382 -0
- package/dist/src/skills/importer.js +921 -0
- package/dist/src/skills/registry.js +19 -0
- package/dist/src/skills/validator.js +323 -0
- package/package.json +2 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { sha256, SessionContractError } from './contracts.js';
|
|
2
|
+
export class FortemiSessionRepositoryBackend {
|
|
3
|
+
options;
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.options = options;
|
|
6
|
+
}
|
|
7
|
+
previewImport(batch) {
|
|
8
|
+
const workspaceIds = [...new Set(batch.sessions.map((session) => session.workspaceId))];
|
|
9
|
+
if (workspaceIds.length !== 1) {
|
|
10
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'optional backend import must contain exactly one workspace scope');
|
|
11
|
+
}
|
|
12
|
+
return repositoryPreview({
|
|
13
|
+
operation: 'upsert',
|
|
14
|
+
workspaceId: workspaceIds[0],
|
|
15
|
+
sourceId: batch.source.sourceId,
|
|
16
|
+
eventIdentities: batch.events.map((event) => `${event.eventId}:${event.digest}`),
|
|
17
|
+
transfersApprovedText: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async applyImport(batch, checkpoint, authorization) {
|
|
21
|
+
this.assertCapabilities();
|
|
22
|
+
const preview = this.previewImport(batch);
|
|
23
|
+
assertRepositoryAuthorization(preview, authorization);
|
|
24
|
+
const result = await this.options.client.callTool('upsert_external_notes', {
|
|
25
|
+
source_namespace: `aiwg.sessions.${batch.source.provider}`,
|
|
26
|
+
source_id: batch.source.sourceId,
|
|
27
|
+
source_schema_version: batch.source.sourceSchemaVersion,
|
|
28
|
+
import_run_id: batch.run.importRunId,
|
|
29
|
+
workspace_id: preview.workspaceId,
|
|
30
|
+
items: batch.events.map((event) => ({
|
|
31
|
+
external_id: event.eventId,
|
|
32
|
+
content: event.searchableText,
|
|
33
|
+
content_digest: event.digest,
|
|
34
|
+
metadata: {
|
|
35
|
+
session_id: event.sessionId,
|
|
36
|
+
import_run_id: event.importRunId,
|
|
37
|
+
locator_class: batch.source.locatorClass,
|
|
38
|
+
role: event.role,
|
|
39
|
+
sensitivity: event.sensitivity.classification,
|
|
40
|
+
},
|
|
41
|
+
})),
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
operationId: batch.run.importRunId,
|
|
45
|
+
outcome: result?.outcome ?? 'committed',
|
|
46
|
+
sessionsInserted: result?.sessions_inserted ?? batch.sessions.length,
|
|
47
|
+
eventsInserted: result?.events_inserted ?? batch.events.length,
|
|
48
|
+
checkpoint,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
previewTombstone(workspaceId, sessionId, eventIds) {
|
|
52
|
+
return repositoryPreview({
|
|
53
|
+
operation: 'tombstone',
|
|
54
|
+
workspaceId,
|
|
55
|
+
sessionId,
|
|
56
|
+
eventIdentities: eventIds,
|
|
57
|
+
transfersApprovedText: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async tombstone(workspaceId, sessionId, eventIds, authorization) {
|
|
61
|
+
this.assertCapabilities(true);
|
|
62
|
+
const preview = this.previewTombstone(workspaceId, sessionId, eventIds);
|
|
63
|
+
assertRepositoryAuthorization(preview, authorization);
|
|
64
|
+
const result = await this.options.client.callTool('purge_external_notes', {
|
|
65
|
+
workspace_id: workspaceId,
|
|
66
|
+
session_id: sessionId,
|
|
67
|
+
external_ids: [...eventIds],
|
|
68
|
+
mode: 'tombstone',
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
operationId: preview.operationId,
|
|
72
|
+
outcome: 'committed',
|
|
73
|
+
affected: result?.affected ?? eventIds.length,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
assertCapabilities(requireGraphPurge = false) {
|
|
77
|
+
if (!this.options.capabilities.sourceAddressedUpsert
|
|
78
|
+
|| !this.options.capabilities.typedMetadataPredicates
|
|
79
|
+
|| !this.options.capabilities.evidenceLocators
|
|
80
|
+
|| (requireGraphPurge && !this.options.capabilities.graphPurge)) {
|
|
81
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', 'Fortemi session repository requires source upsert, typed predicates, evidence locators, and graph purge for deletion');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class FortemiSessionBackend {
|
|
86
|
+
options;
|
|
87
|
+
id = 'fortemi';
|
|
88
|
+
requiresNetwork = true;
|
|
89
|
+
requiresModel = true;
|
|
90
|
+
constructor(options) {
|
|
91
|
+
this.options = options;
|
|
92
|
+
}
|
|
93
|
+
async rank(request) {
|
|
94
|
+
if (!this.options.capabilities.sourceAddressedUpsert
|
|
95
|
+
|| !this.options.capabilities.typedMetadataPredicates
|
|
96
|
+
|| !this.options.capabilities.evidenceLocators) {
|
|
97
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', 'Fortemi session integration requires source upsert, typed predicates, and evidence locators');
|
|
98
|
+
}
|
|
99
|
+
const result = await this.options.client.callTool('search', {
|
|
100
|
+
query: request.query,
|
|
101
|
+
mode: 'hybrid',
|
|
102
|
+
predicates: [{ path: 'workspace_id', op: 'eq', value: request.workspaceId }],
|
|
103
|
+
candidates: request.documents.map((document) => ({
|
|
104
|
+
external_id: document.eventId,
|
|
105
|
+
content: document.text,
|
|
106
|
+
citation: document.citation,
|
|
107
|
+
})),
|
|
108
|
+
});
|
|
109
|
+
return (result?.results ?? [])
|
|
110
|
+
.filter((entry) => typeof entry.external_id === 'string' && typeof entry.score === 'number')
|
|
111
|
+
.map((entry) => ({ eventId: entry.external_id, score: entry.score }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class SessionSearchService {
|
|
115
|
+
lexical;
|
|
116
|
+
constructor(lexical) {
|
|
117
|
+
this.lexical = lexical;
|
|
118
|
+
}
|
|
119
|
+
preview(options, backend) {
|
|
120
|
+
const documents = this.documents(options);
|
|
121
|
+
return createPreview(options, backend, documents);
|
|
122
|
+
}
|
|
123
|
+
async search(request) {
|
|
124
|
+
if ((request.mode ?? 'lexical') === 'lexical' || !request.backend) {
|
|
125
|
+
return this.lexical.search(request.options);
|
|
126
|
+
}
|
|
127
|
+
const documents = this.documents(request.options);
|
|
128
|
+
const preview = createPreview(request.options, request.backend, documents);
|
|
129
|
+
if (!request.authorization?.approved
|
|
130
|
+
|| request.authorization.operationId !== preview.operationId) {
|
|
131
|
+
throw new SessionContractError(request.backend.requiresNetwork ? 'NETWORK_NOT_AUTHORIZED' : 'OPERATION_NOT_AUTHORIZED', 'optional semantic/backend search requires approval of the exact preview operation');
|
|
132
|
+
}
|
|
133
|
+
const ranked = await request.backend.rank({
|
|
134
|
+
query: request.options.query,
|
|
135
|
+
workspaceId: request.options.workspaceId,
|
|
136
|
+
documents: documents.map((document) => ({
|
|
137
|
+
eventId: document.eventId,
|
|
138
|
+
text: document.searchableText,
|
|
139
|
+
citation: document.citation,
|
|
140
|
+
})),
|
|
141
|
+
});
|
|
142
|
+
const authorized = new Map(documents.map((document) => [document.eventId, document]));
|
|
143
|
+
const semanticByEvent = new Map();
|
|
144
|
+
for (const candidate of ranked) {
|
|
145
|
+
if (!Number.isFinite(candidate.score) || !authorized.has(candidate.eventId))
|
|
146
|
+
continue;
|
|
147
|
+
semanticByEvent.set(candidate.eventId, Math.max(semanticByEvent.get(candidate.eventId) ?? Number.NEGATIVE_INFINITY, candidate.score));
|
|
148
|
+
}
|
|
149
|
+
const semantic = [...semanticByEvent.entries()]
|
|
150
|
+
.map(([eventId, score]) => ({ eventId, score }))
|
|
151
|
+
.sort((left, right) => right.score - left.score || left.eventId.localeCompare(right.eventId));
|
|
152
|
+
const lexical = this.lexical.search({
|
|
153
|
+
...request.options,
|
|
154
|
+
limit: Math.min(500, Math.max(request.options.limit * 2, request.options.limit)),
|
|
155
|
+
cursor: undefined,
|
|
156
|
+
}).items;
|
|
157
|
+
const scores = new Map();
|
|
158
|
+
lexical.forEach((hit, index) => scores.set(hit.eventId, 1 / (60 + index + 1)));
|
|
159
|
+
semantic.forEach((hit, index) => {
|
|
160
|
+
scores.set(hit.eventId, (scores.get(hit.eventId) ?? 0) + 1 / (60 + index + 1));
|
|
161
|
+
});
|
|
162
|
+
const publicHits = new Map([
|
|
163
|
+
...documents.map(({ searchableText: _text, ...hit }) => [hit.eventId, hit]),
|
|
164
|
+
...lexical.map((hit) => [hit.eventId, hit]),
|
|
165
|
+
]);
|
|
166
|
+
const items = [...scores.entries()]
|
|
167
|
+
.map(([eventId, score]) => ({ hit: publicHits.get(eventId), score }))
|
|
168
|
+
.filter((entry) => Boolean(entry.hit))
|
|
169
|
+
.sort((left, right) => right.score - left.score
|
|
170
|
+
|| left.hit.eventId.localeCompare(right.hit.eventId))
|
|
171
|
+
.slice(0, request.options.limit)
|
|
172
|
+
.map(({ hit, score }) => ({ ...hit, score }));
|
|
173
|
+
return { items, nextCursor: null };
|
|
174
|
+
}
|
|
175
|
+
documents(options) {
|
|
176
|
+
const { query: _query, cursor: _cursor, ...scope } = options;
|
|
177
|
+
return this.lexical.authorizedSearchDocuments({
|
|
178
|
+
...scope,
|
|
179
|
+
limit: Math.min(500, Math.max(100, options.limit * 20)),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function createPreview(options, backend, documents) {
|
|
184
|
+
const operationId = sha256(JSON.stringify({
|
|
185
|
+
backend: backend.id,
|
|
186
|
+
mode: 'hybrid',
|
|
187
|
+
workspaceId: options.workspaceId,
|
|
188
|
+
queryDigest: sha256(options.query),
|
|
189
|
+
scopeDigest: sha256(JSON.stringify({
|
|
190
|
+
providers: options.providers ?? [],
|
|
191
|
+
dateFrom: options.dateFrom ?? null,
|
|
192
|
+
dateTo: options.dateTo ?? null,
|
|
193
|
+
participant: options.participant ?? null,
|
|
194
|
+
model: options.model ?? null,
|
|
195
|
+
role: options.role ?? null,
|
|
196
|
+
tool: options.tool ?? null,
|
|
197
|
+
tag: options.tag ?? null,
|
|
198
|
+
entity: options.entity ?? null,
|
|
199
|
+
sensitivity: options.sensitivity ?? null,
|
|
200
|
+
extractionState: options.extractionState ?? null,
|
|
201
|
+
limit: options.limit,
|
|
202
|
+
})),
|
|
203
|
+
eventIds: documents.map((document) => document.eventId),
|
|
204
|
+
requiresNetwork: backend.requiresNetwork,
|
|
205
|
+
requiresModel: backend.requiresModel,
|
|
206
|
+
}));
|
|
207
|
+
return {
|
|
208
|
+
contractVersion: '1.0.0',
|
|
209
|
+
operationId,
|
|
210
|
+
backend: backend.id,
|
|
211
|
+
mode: 'hybrid',
|
|
212
|
+
workspaceId: options.workspaceId,
|
|
213
|
+
candidateCount: documents.length,
|
|
214
|
+
transfersApprovedText: true,
|
|
215
|
+
requiresNetwork: backend.requiresNetwork,
|
|
216
|
+
requiresModel: backend.requiresModel,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function repositoryPreview(input) {
|
|
220
|
+
return {
|
|
221
|
+
contractVersion: '1.0.0',
|
|
222
|
+
operationId: sha256(JSON.stringify(input)),
|
|
223
|
+
backend: 'fortemi',
|
|
224
|
+
operation: input.operation,
|
|
225
|
+
workspaceId: input.workspaceId,
|
|
226
|
+
sourceId: input.sourceId,
|
|
227
|
+
sessionId: input.sessionId,
|
|
228
|
+
eventCount: input.eventIdentities.length,
|
|
229
|
+
transfersApprovedText: input.transfersApprovedText,
|
|
230
|
+
requiresNetwork: true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function assertRepositoryAuthorization(preview, authorization) {
|
|
234
|
+
if (!authorization?.approved || authorization.operationId !== preview.operationId) {
|
|
235
|
+
throw new SessionContractError('NETWORK_NOT_AUTHORIZED', 'optional repository operation requires approval of the exact preview operation');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=optional-backends.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export const ORIGIN_CLASSIFIER_VERSION = '1.0.0';
|
|
2
|
+
export function classifySessionEventOrigin(provider, record) {
|
|
3
|
+
const envelope = classifyWholeEnvelope(record.text);
|
|
4
|
+
if (envelope)
|
|
5
|
+
return envelope;
|
|
6
|
+
const kind = record.kind.toLowerCase();
|
|
7
|
+
if (isProviderBootstrap(provider, kind)) {
|
|
8
|
+
return classified('provider-bootstrap', `${provider}:structured-bootstrap`);
|
|
9
|
+
}
|
|
10
|
+
if (isToolControl(kind, record.role)) {
|
|
11
|
+
return classified('tool-control', `${provider}:structured-control`);
|
|
12
|
+
}
|
|
13
|
+
if (record.role === 'assistant') {
|
|
14
|
+
return classified('assistant-generated', `${provider}:assistant-role`);
|
|
15
|
+
}
|
|
16
|
+
if (record.role === 'user' && kind === 'message' && record.text.trim().length > 0) {
|
|
17
|
+
return classified('user-authored', `${provider}:user-message`);
|
|
18
|
+
}
|
|
19
|
+
return classified('unknown', `${provider}:insufficient-authorship-evidence`);
|
|
20
|
+
}
|
|
21
|
+
export function isControlOrigin(origin) {
|
|
22
|
+
return origin === 'provider-bootstrap'
|
|
23
|
+
|| origin === 'workspace-instruction'
|
|
24
|
+
|| origin === 'tool-control';
|
|
25
|
+
}
|
|
26
|
+
export function deriveSessionIntent(events) {
|
|
27
|
+
const eligible = events
|
|
28
|
+
.filter((event) => event.origin === 'user-authored'
|
|
29
|
+
&& event.kind === 'message'
|
|
30
|
+
&& event.searchableText.trim().length > 0)
|
|
31
|
+
.sort((left, right) => left.sequence - right.sequence || left.eventId.localeCompare(right.eventId));
|
|
32
|
+
const selected = eligible[0];
|
|
33
|
+
if (selected) {
|
|
34
|
+
const summary = normalizeExcerpt(selected.searchableText, 240);
|
|
35
|
+
return {
|
|
36
|
+
status: 'selected',
|
|
37
|
+
eventId: selected.eventId,
|
|
38
|
+
sequence: selected.sequence,
|
|
39
|
+
title: normalizeExcerpt(summary.split('\n')[0], 80),
|
|
40
|
+
summary,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const uncertain = events.some((event) => event.origin === 'unknown' && event.searchableText.trim().length > 0);
|
|
44
|
+
return {
|
|
45
|
+
status: uncertain ? 'unknown' : 'absent',
|
|
46
|
+
eventId: null,
|
|
47
|
+
sequence: null,
|
|
48
|
+
title: null,
|
|
49
|
+
summary: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function classifyWholeEnvelope(text) {
|
|
53
|
+
const trimmed = text.trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
return null;
|
|
56
|
+
if (/^# AGENTS\.md instructions for [^\n]+\n+<INSTRUCTIONS>\n[\s\S]*\n<\/INSTRUCTIONS>$/.test(trimmed)) {
|
|
57
|
+
return classified('workspace-instruction', 'envelope:agents-instructions');
|
|
58
|
+
}
|
|
59
|
+
for (const tag of [
|
|
60
|
+
'recommended_plugins',
|
|
61
|
+
'codex_internal_context',
|
|
62
|
+
'environment_context',
|
|
63
|
+
]) {
|
|
64
|
+
if (wholeTag(trimmed, tag)) {
|
|
65
|
+
return classified('provider-bootstrap', `envelope:${tag}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (wholeTag(trimmed, 'local-command-caveat')) {
|
|
69
|
+
return classified('tool-control', 'envelope:local-command-caveat');
|
|
70
|
+
}
|
|
71
|
+
if (/^<[A-Za-z_][\w.-]*(?:\s[^>]*)?>[\s\S]*<\/[A-Za-z_][\w.-]*>$/.test(trimmed)) {
|
|
72
|
+
return classified('unknown', 'envelope:unrecognized');
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function wholeTag(value, tag) {
|
|
77
|
+
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
78
|
+
return new RegExp(`^<${escaped}(?:\\s[^>]*)?>[\\s\\S]*<\\/${escaped}>$`).test(value);
|
|
79
|
+
}
|
|
80
|
+
function isProviderBootstrap(provider, kind) {
|
|
81
|
+
if (provider === 'codex') {
|
|
82
|
+
return kind === 'codex.session_meta'
|
|
83
|
+
|| kind === 'codex.turn_context'
|
|
84
|
+
|| kind === 'codex.thread-state';
|
|
85
|
+
}
|
|
86
|
+
if (provider === 'factory') {
|
|
87
|
+
return kind === 'factory.session_start' || kind === 'factory.settings';
|
|
88
|
+
}
|
|
89
|
+
if (provider === 'cursor')
|
|
90
|
+
return kind === 'system';
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function isToolControl(kind, role) {
|
|
94
|
+
return role === 'tool'
|
|
95
|
+
|| role === 'system'
|
|
96
|
+
|| kind === 'lifecycle-hook'
|
|
97
|
+
|| kind === 'tool-call'
|
|
98
|
+
|| kind === 'tool-result'
|
|
99
|
+
|| kind === 'summary'
|
|
100
|
+
|| kind.includes('lifecycle')
|
|
101
|
+
|| kind.startsWith('tool.')
|
|
102
|
+
|| kind.startsWith('cursor.cloud.')
|
|
103
|
+
|| kind === 'cursor.agent.turn_ended'
|
|
104
|
+
|| kind === 'factory.session_end';
|
|
105
|
+
}
|
|
106
|
+
function classified(origin, rule) {
|
|
107
|
+
return {
|
|
108
|
+
origin,
|
|
109
|
+
rule,
|
|
110
|
+
classifierVersion: ORIGIN_CLASSIFIER_VERSION,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function normalizeExcerpt(value, max) {
|
|
114
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
115
|
+
return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1)}…`;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=origin.js.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { SessionContractError } from './contracts.js';
|
|
4
|
+
export async function authorizeSourceFile(input) {
|
|
5
|
+
if (input.allowedRoots.length === 0) {
|
|
6
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'an explicitly selected allowed root is required');
|
|
7
|
+
}
|
|
8
|
+
const selected = resolve(input.selectedPath);
|
|
9
|
+
const selectedStat = await safeLstat(selected);
|
|
10
|
+
if (selectedStat.isSymbolicLink() && !input.allowSymlink) {
|
|
11
|
+
throw new SessionContractError('SOURCE_SYMLINK', 'symbolic-link session sources are disabled');
|
|
12
|
+
}
|
|
13
|
+
const canonicalPath = await safeRealpath(selected);
|
|
14
|
+
const roots = await Promise.all(input.allowedRoots.map(async (root) => ({
|
|
15
|
+
input: root,
|
|
16
|
+
canonical: await safeRealpath(resolve(root), true),
|
|
17
|
+
})));
|
|
18
|
+
const matchingRoot = roots.find(({ canonical }) => {
|
|
19
|
+
const rel = relative(canonical, canonicalPath);
|
|
20
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
21
|
+
});
|
|
22
|
+
if (!matchingRoot) {
|
|
23
|
+
throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', 'source is outside the explicitly allowed roots');
|
|
24
|
+
}
|
|
25
|
+
const stat = await safeLstat(canonicalPath);
|
|
26
|
+
if (!stat.isFile()) {
|
|
27
|
+
throw new SessionContractError('SOURCE_NOT_REGULAR_FILE', 'session source must be a regular file');
|
|
28
|
+
}
|
|
29
|
+
if (input.maxBytes !== undefined && stat.size > input.maxBytes) {
|
|
30
|
+
throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'session source exceeds the authorized byte limit');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
canonicalPath,
|
|
34
|
+
rootClass: matchingRoot.input,
|
|
35
|
+
size: stat.size,
|
|
36
|
+
mtimeMs: stat.mtimeMs,
|
|
37
|
+
dev: stat.dev,
|
|
38
|
+
ino: stat.ino,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function safeLstat(path) {
|
|
42
|
+
try {
|
|
43
|
+
return await lstat(path);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'selected session source is inaccessible');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function safeRealpath(path, root = false) {
|
|
50
|
+
try {
|
|
51
|
+
return await realpath(path);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', root ? 'an authorized source root is inaccessible' : 'selected session source is inaccessible');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const SECRET_PATTERNS = [
|
|
58
|
+
['private-key', /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g],
|
|
59
|
+
['bearer-token', /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/gi],
|
|
60
|
+
['authorization', /\bAuthorization\s*:\s*[^\r\n]+/gi],
|
|
61
|
+
['cookie', /\b(?:Cookie|Set-Cookie)\s*:\s*[^\r\n]+/gi],
|
|
62
|
+
['credential', /\b(?:api[_-]?key|token|password|secret)\s*(?:[:=]|\s)\s*["']?[^\s"',;]{6,}/gi],
|
|
63
|
+
['provider-token', /\b(?:sk-(?:proj-)?|gh[pousr]_|github_pat_|xox[baprs]-|AKIA)[A-Za-z0-9._~+/=-]{8,}\b/g],
|
|
64
|
+
['connection-string', /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s]+/gi],
|
|
65
|
+
['credential-url', /\bhttps?:\/\/[^/\s:@]+:[^/\s@]+@[^\s]+/gi],
|
|
66
|
+
['environment-assignment', /\b[A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)\s*=\s*(?:"[^"]+"|'[^']+'|[^\s]+)/g],
|
|
67
|
+
['fixture-credential', /\bredaction-canary-[A-Za-z0-9_-]+\b/gi],
|
|
68
|
+
['email', /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi],
|
|
69
|
+
];
|
|
70
|
+
const SENSITIVE_KEY = /(?:authorization|auth[_-]?header|cookie|credential|password|passwd|secret|token|api[_-]?key|private[_-]?key|connection[_-]?string)/i;
|
|
71
|
+
const CONTENT_KEY = /(?:^|[_-])(?:text|content|prompt|command|arguments?|args|result|output|body|request|response|source|code|query)(?:$|[_-])/i;
|
|
72
|
+
const PATH_KEY = /(?:^|[_-])(?:path|cwd|directory|filename|file)(?:$|[_-])/i;
|
|
73
|
+
const SAFE_STRING_KEY = /(?:^|[_-])(?:id|kind|type|role|status|state|lifecycle|reason|name|product|provider|version|schema|model|format|class|mode|event|operation|tool|method|language|scope|consistency|disposition|phase|visibility|protocol|category|classification)(?:$|[_-])/i;
|
|
74
|
+
const CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
75
|
+
const DEFAULT_NATIVE_LIMITS = {
|
|
76
|
+
maxDepth: 8,
|
|
77
|
+
maxNodes: 2_048,
|
|
78
|
+
maxObjectKeys: 128,
|
|
79
|
+
maxArrayItems: 256,
|
|
80
|
+
maxStringBytes: 8 * 1024,
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Classify and sanitize provider-native attributes before they cross the
|
|
84
|
+
* persistence boundary. Decisions contain counts only and are safe for audit.
|
|
85
|
+
*/
|
|
86
|
+
export function sanitizeNativeExtensions(input, overrides = {}) {
|
|
87
|
+
const limits = { ...DEFAULT_NATIVE_LIMITS, ...overrides };
|
|
88
|
+
const classes = new Set();
|
|
89
|
+
const decisions = {};
|
|
90
|
+
const active = new WeakSet();
|
|
91
|
+
let nodes = 0;
|
|
92
|
+
const marker = (classification) => {
|
|
93
|
+
classes.add(classification);
|
|
94
|
+
decisions[classification] = (decisions[classification] ?? 0) + 1;
|
|
95
|
+
return `[REDACTED:${classification}]`;
|
|
96
|
+
};
|
|
97
|
+
const visit = (value, key, depth) => {
|
|
98
|
+
const classifiedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
|
99
|
+
nodes += 1;
|
|
100
|
+
if (nodes > limits.maxNodes)
|
|
101
|
+
return marker('node-limit');
|
|
102
|
+
if (depth > limits.maxDepth)
|
|
103
|
+
return marker('depth-limit');
|
|
104
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
105
|
+
return value;
|
|
106
|
+
if (typeof value === 'bigint')
|
|
107
|
+
return String(value);
|
|
108
|
+
if (typeof value !== 'string' && typeof value !== 'object') {
|
|
109
|
+
return marker('unsupported-value');
|
|
110
|
+
}
|
|
111
|
+
if (typeof value === 'string') {
|
|
112
|
+
if (SENSITIVE_KEY.test(classifiedKey))
|
|
113
|
+
return marker('sensitive-field');
|
|
114
|
+
if (CONTENT_KEY.test(classifiedKey))
|
|
115
|
+
return marker('content');
|
|
116
|
+
if (PATH_KEY.test(classifiedKey))
|
|
117
|
+
return marker('path');
|
|
118
|
+
if (Buffer.byteLength(value) > limits.maxStringBytes)
|
|
119
|
+
return marker('string-limit');
|
|
120
|
+
const redacted = redactSessionText(value);
|
|
121
|
+
for (const classification of redacted.classes)
|
|
122
|
+
classes.add(classification);
|
|
123
|
+
if (redacted.classes.length) {
|
|
124
|
+
decisions['classified-value'] = (decisions['classified-value'] ?? 0) + 1;
|
|
125
|
+
return redacted.text.replace(CONTROL_CHARACTERS, ' ');
|
|
126
|
+
}
|
|
127
|
+
if (!SAFE_STRING_KEY.test(classifiedKey))
|
|
128
|
+
return marker('unclassified-field');
|
|
129
|
+
return value.replace(/\r?\n/g, ' ').replace(CONTROL_CHARACTERS, ' ');
|
|
130
|
+
}
|
|
131
|
+
if (active.has(value))
|
|
132
|
+
return marker('circular-reference');
|
|
133
|
+
active.add(value);
|
|
134
|
+
try {
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
const selected = value.slice(0, limits.maxArrayItems)
|
|
137
|
+
.map((entry) => visit(entry, key, depth + 1));
|
|
138
|
+
if (value.length > limits.maxArrayItems)
|
|
139
|
+
selected.push(marker('array-limit'));
|
|
140
|
+
return selected;
|
|
141
|
+
}
|
|
142
|
+
const output = {};
|
|
143
|
+
const entries = Object.entries(value);
|
|
144
|
+
for (const [childKey, childValue] of entries.slice(0, limits.maxObjectKeys)) {
|
|
145
|
+
output[childKey.replace(/\r?\n/g, ' ').replace(CONTROL_CHARACTERS, '')] =
|
|
146
|
+
visit(childValue, childKey, depth + 1);
|
|
147
|
+
}
|
|
148
|
+
if (entries.length > limits.maxObjectKeys)
|
|
149
|
+
output.__truncated__ = marker('object-key-limit');
|
|
150
|
+
return output;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
active.delete(value);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const visited = visit(input ?? {}, 'extensions', 0);
|
|
157
|
+
const value = visited && typeof visited === 'object' && !Array.isArray(visited)
|
|
158
|
+
? visited
|
|
159
|
+
: { value: visited };
|
|
160
|
+
return {
|
|
161
|
+
value,
|
|
162
|
+
sensitivity: classes.size ? 'sensitive' : 'none',
|
|
163
|
+
classes: [...classes].sort(),
|
|
164
|
+
decisions,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function redactSessionText(input) {
|
|
168
|
+
const classes = new Set();
|
|
169
|
+
let text = input;
|
|
170
|
+
for (const [classification, pattern] of SECRET_PATTERNS) {
|
|
171
|
+
text = text.replace(pattern, () => {
|
|
172
|
+
classes.add(classification);
|
|
173
|
+
return `[REDACTED:${classification}]`;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return { text, sensitivity: classes.size ? 'sensitive' : 'none', classes: [...classes].sort() };
|
|
177
|
+
}
|
|
178
|
+
export function contentFreeAuditEvent(event) {
|
|
179
|
+
return Object.freeze({ ...event, counts: { ...event.counts } });
|
|
180
|
+
}
|
|
181
|
+
export function requireNetworkConsent(operation, authorizedOperation) {
|
|
182
|
+
if (authorizedOperation !== operation) {
|
|
183
|
+
throw new SessionContractError('NETWORK_NOT_AUTHORIZED', `network operation requires explicit consent: ${operation}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Apply authorization before a search implementation computes rank or snippets. */
|
|
187
|
+
export function prefilterAuthorizedSearchScope(records, scope) {
|
|
188
|
+
const providers = scope.providers ? new Set(scope.providers) : null;
|
|
189
|
+
return records.filter((record) => record.workspaceId === scope.workspaceId
|
|
190
|
+
&& (providers === null || providers.has(record.provider)));
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=policy.js.map
|