sentinelayer-cli 0.4.5 → 0.6.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/README.md +996 -998
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +63 -63
- package/src/agents/jules/config/definition.js +160 -209
- package/src/agents/jules/config/system-prompt.js +182 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +17 -377
- package/src/agents/jules/loop.js +450 -367
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +186 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +309 -308
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1691 -557
- package/src/agents/jules/tools/dispatch.js +335 -327
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +507 -503
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/jules/tools/url-policy.js +100 -100
- package/src/agents/persona-visuals.js +61 -0
- package/src/agents/shared-tools/dispatch-core.js +315 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +1009 -972
- package/src/ai/client.js +553 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/proxy.js +137 -0
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +371 -126
- package/src/auth/http.js +611 -270
- package/src/auth/service.js +1106 -891
- package/src/auth/session-store.js +813 -359
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1338
- package/src/commands/ai/provision-governance.js +1272 -1272
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1166 -1166
- package/src/commands/auth.js +419 -375
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -461
- package/src/commands/omargate.js +29 -21
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +872 -866
- package/src/commands/spec.js +716 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +511 -510
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +770 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +626 -626
- package/src/daemon/fix-cycle.js +377 -0
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/watchdog.js +971 -971
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/interactive/index.js +97 -95
- package/src/legacy-cli.js +2994 -2592
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +118 -118
- package/src/review/ai-review.js +679 -669
- package/src/review/local-review.js +1305 -1295
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +300 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/scan-modes.js +42 -0
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -67
- package/src/scaffold/templates.js +150 -150
- package/src/scan/generator.js +418 -418
- package/src/scan/gh-secrets.js +107 -107
- package/src/spec/generator.js +519 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/session-tracker.js +234 -118
- package/src/telemetry/sync.js +203 -199
- package/src/ui/command-hints.js +13 -0
- package/src/ui/markdown.js +220 -220
package/src/ai/identity-store.js
CHANGED
|
@@ -1,270 +1,270 @@
|
|
|
1
|
-
import fsp from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const REGISTRY_SCHEMA_VERSION = "1.0.0";
|
|
5
|
-
const TERMINAL_IDENTITY_STATUSES = new Set(["SQUASHED"]);
|
|
6
|
-
|
|
7
|
-
function normalizeString(value) {
|
|
8
|
-
return String(value || "").trim();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function normalizeUpper(value) {
|
|
12
|
-
return normalizeString(value).toUpperCase();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizeTagList(value) {
|
|
16
|
-
if (!Array.isArray(value)) {
|
|
17
|
-
return [];
|
|
18
|
-
}
|
|
19
|
-
const unique = new Set();
|
|
20
|
-
for (const item of value) {
|
|
21
|
-
const normalized = normalizeString(item);
|
|
22
|
-
if (normalized) {
|
|
23
|
-
unique.add(normalized);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return [...unique];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function normalizeLegalHoldStatus(value) {
|
|
30
|
-
const normalized = normalizeUpper(value);
|
|
31
|
-
if (!normalized) {
|
|
32
|
-
return "NONE";
|
|
33
|
-
}
|
|
34
|
-
if (normalized === "HOLD" || normalized === "NONE" || normalized === "UNKNOWN") {
|
|
35
|
-
return normalized;
|
|
36
|
-
}
|
|
37
|
-
return normalized;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizeMetadata(value) {
|
|
41
|
-
const source = value && typeof value === "object" ? value : {};
|
|
42
|
-
return {
|
|
43
|
-
...source,
|
|
44
|
-
tags: normalizeTagList(source.tags),
|
|
45
|
-
legalHoldStatus: normalizeLegalHoldStatus(source.legalHoldStatus),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function normalizeIdentityRecord(record = {}) {
|
|
50
|
-
const metadata = normalizeMetadata(record.metadata);
|
|
51
|
-
const legalHoldStatus = normalizeLegalHoldStatus(record.legalHoldStatus || metadata.legalHoldStatus);
|
|
52
|
-
return {
|
|
53
|
-
identityId: normalizeString(record.identityId),
|
|
54
|
-
parentIdentityId: normalizeString(record.parentIdentityId) || null,
|
|
55
|
-
emailAddress: normalizeString(record.emailAddress) || null,
|
|
56
|
-
status: normalizeString(record.status) || "UNKNOWN",
|
|
57
|
-
projectId: normalizeString(record.projectId) || null,
|
|
58
|
-
orgId: normalizeString(record.orgId) || null,
|
|
59
|
-
apiUrl: normalizeString(record.apiUrl) || null,
|
|
60
|
-
createdAt: normalizeString(record.createdAt) || new Date().toISOString(),
|
|
61
|
-
lastUpdatedAt: normalizeString(record.lastUpdatedAt) || new Date().toISOString(),
|
|
62
|
-
expiresAt: normalizeString(record.expiresAt) || null,
|
|
63
|
-
revokedAt: normalizeString(record.revokedAt) || null,
|
|
64
|
-
squashedAt: normalizeString(record.squashedAt) || null,
|
|
65
|
-
legalHoldStatus,
|
|
66
|
-
metadata,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function resolveIdentityRegistryPath({ outputRoot } = {}) {
|
|
71
|
-
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
72
|
-
return path.join(resolvedOutputRoot, "aidenid", "identity-registry.json");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function loadRegistryInternal(filePath) {
|
|
76
|
-
try {
|
|
77
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
78
|
-
const parsed = JSON.parse(raw);
|
|
79
|
-
const identities = Array.isArray(parsed.identities)
|
|
80
|
-
? parsed.identities.map((item) => normalizeIdentityRecord(item)).filter((item) => item.identityId)
|
|
81
|
-
: [];
|
|
82
|
-
return {
|
|
83
|
-
schemaVersion: normalizeString(parsed.schemaVersion) || REGISTRY_SCHEMA_VERSION,
|
|
84
|
-
generatedAt: normalizeString(parsed.generatedAt) || new Date().toISOString(),
|
|
85
|
-
identities,
|
|
86
|
-
};
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
89
|
-
return {
|
|
90
|
-
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
91
|
-
generatedAt: new Date().toISOString(),
|
|
92
|
-
identities: [],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function writeRegistryInternal(filePath, registry = {}) {
|
|
100
|
-
const normalized = {
|
|
101
|
-
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
102
|
-
generatedAt: new Date().toISOString(),
|
|
103
|
-
identities: Array.isArray(registry.identities) ? registry.identities.map(normalizeIdentityRecord) : [],
|
|
104
|
-
};
|
|
105
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
106
|
-
await fsp.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
107
|
-
return normalized;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function listIdentities({ outputRoot } = {}) {
|
|
111
|
-
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
112
|
-
const registry = await loadRegistryInternal(registryPath);
|
|
113
|
-
return {
|
|
114
|
-
registryPath,
|
|
115
|
-
identities: registry.identities,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function getIdentityById({ outputRoot, identityId } = {}) {
|
|
120
|
-
const normalizedIdentityId = normalizeString(identityId);
|
|
121
|
-
if (!normalizedIdentityId) {
|
|
122
|
-
throw new Error("identityId is required.");
|
|
123
|
-
}
|
|
124
|
-
const { registryPath, identities } = await listIdentities({ outputRoot });
|
|
125
|
-
const identity = identities.find((item) => item.identityId === normalizedIdentityId) || null;
|
|
126
|
-
return {
|
|
127
|
-
registryPath,
|
|
128
|
-
identity,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export async function recordProvisionedIdentity({
|
|
133
|
-
outputRoot,
|
|
134
|
-
response = {},
|
|
135
|
-
context = {},
|
|
136
|
-
} = {}) {
|
|
137
|
-
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
138
|
-
const registry = await loadRegistryInternal(registryPath);
|
|
139
|
-
const identityId = normalizeString(response.id);
|
|
140
|
-
if (!identityId) {
|
|
141
|
-
throw new Error("Cannot record identity without response.id.");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const nowIso = new Date().toISOString();
|
|
145
|
-
const source = normalizeString(context.source) || "provision-email";
|
|
146
|
-
const nextRecord = normalizeIdentityRecord({
|
|
147
|
-
identityId,
|
|
148
|
-
parentIdentityId: response.parentIdentityId || context.parentIdentityId || null,
|
|
149
|
-
emailAddress: response.emailAddress,
|
|
150
|
-
status: response.status || "ACTIVE",
|
|
151
|
-
projectId: response.projectId || context.projectId,
|
|
152
|
-
orgId: context.orgId,
|
|
153
|
-
apiUrl: context.apiUrl,
|
|
154
|
-
createdAt: nowIso,
|
|
155
|
-
lastUpdatedAt: nowIso,
|
|
156
|
-
expiresAt: response.expiresAt || null,
|
|
157
|
-
legalHoldStatus: response.legalHoldStatus || context.legalHoldStatus || "NONE",
|
|
158
|
-
metadata: {
|
|
159
|
-
source,
|
|
160
|
-
idempotencyKey: context.idempotencyKey || null,
|
|
161
|
-
eventBudget: context.eventBudget ?? null,
|
|
162
|
-
tags: normalizeTagList(context.tags || response.tags || []),
|
|
163
|
-
legalHoldStatus: response.legalHoldStatus || context.legalHoldStatus || "NONE",
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const index = registry.identities.findIndex((item) => item.identityId === identityId);
|
|
168
|
-
if (index >= 0) {
|
|
169
|
-
const existing = registry.identities[index];
|
|
170
|
-
registry.identities[index] = normalizeIdentityRecord({
|
|
171
|
-
...existing,
|
|
172
|
-
...nextRecord,
|
|
173
|
-
createdAt: existing.createdAt || nextRecord.createdAt,
|
|
174
|
-
metadata: {
|
|
175
|
-
...existing.metadata,
|
|
176
|
-
...nextRecord.metadata,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
} else {
|
|
180
|
-
registry.identities.push(nextRecord);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const saved = await writeRegistryInternal(registryPath, registry);
|
|
184
|
-
const identity = saved.identities.find((item) => item.identityId === identityId) || null;
|
|
185
|
-
return {
|
|
186
|
-
registryPath,
|
|
187
|
-
identity,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function updateIdentityStatus({
|
|
192
|
-
outputRoot,
|
|
193
|
-
identityId,
|
|
194
|
-
status,
|
|
195
|
-
revokedAt = "",
|
|
196
|
-
squashedAt = "",
|
|
197
|
-
metadataPatch = {},
|
|
198
|
-
} = {}) {
|
|
199
|
-
const normalizedIdentityId = normalizeString(identityId);
|
|
200
|
-
if (!normalizedIdentityId) {
|
|
201
|
-
throw new Error("identityId is required.");
|
|
202
|
-
}
|
|
203
|
-
const nextStatus = normalizeString(status);
|
|
204
|
-
if (!nextStatus) {
|
|
205
|
-
throw new Error("status is required.");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
209
|
-
const registry = await loadRegistryInternal(registryPath);
|
|
210
|
-
const index = registry.identities.findIndex((item) => item.identityId === normalizedIdentityId);
|
|
211
|
-
if (index < 0) {
|
|
212
|
-
throw new Error(`Identity '${normalizedIdentityId}' was not found in local registry.`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const existing = registry.identities[index];
|
|
216
|
-
registry.identities[index] = normalizeIdentityRecord({
|
|
217
|
-
...existing,
|
|
218
|
-
status: nextStatus,
|
|
219
|
-
revokedAt: normalizeString(revokedAt) || existing.revokedAt || null,
|
|
220
|
-
squashedAt: normalizeString(squashedAt) || existing.squashedAt || null,
|
|
221
|
-
legalHoldStatus: metadataPatch?.legalHoldStatus || existing.legalHoldStatus || "NONE",
|
|
222
|
-
lastUpdatedAt: new Date().toISOString(),
|
|
223
|
-
metadata: {
|
|
224
|
-
...existing.metadata,
|
|
225
|
-
...(metadataPatch && typeof metadataPatch === "object" ? metadataPatch : {}),
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const saved = await writeRegistryInternal(registryPath, registry);
|
|
230
|
-
return {
|
|
231
|
-
registryPath,
|
|
232
|
-
identity: saved.identities[index],
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function filterIdentitiesByTags(identities = [], tags = []) {
|
|
237
|
-
const requestedTags = normalizeTagList(tags);
|
|
238
|
-
if (requestedTags.length === 0) {
|
|
239
|
-
return [...identities];
|
|
240
|
-
}
|
|
241
|
-
const required = new Set(requestedTags);
|
|
242
|
-
return identities.filter((identity) => {
|
|
243
|
-
const identityTags = normalizeTagList(identity?.metadata?.tags);
|
|
244
|
-
if (identityTags.length === 0) {
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
return [...required].every((tag) => identityTags.includes(tag));
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export async function findStaleIdentities({ outputRoot, nowIso = new Date().toISOString() } = {}) {
|
|
252
|
-
const { registryPath, identities } = await listIdentities({ outputRoot });
|
|
253
|
-
const nowMs = new Date(nowIso).getTime();
|
|
254
|
-
const stale = identities.filter((identity) => {
|
|
255
|
-
const status = normalizeUpper(identity.status);
|
|
256
|
-
if (TERMINAL_IDENTITY_STATUSES.has(status)) {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
const expiresAtMs = new Date(identity.expiresAt || "").getTime();
|
|
260
|
-
if (!Number.isFinite(expiresAtMs)) {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
return expiresAtMs <= nowMs;
|
|
264
|
-
});
|
|
265
|
-
return {
|
|
266
|
-
registryPath,
|
|
267
|
-
identities,
|
|
268
|
-
stale,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const REGISTRY_SCHEMA_VERSION = "1.0.0";
|
|
5
|
+
const TERMINAL_IDENTITY_STATUSES = new Set(["SQUASHED"]);
|
|
6
|
+
|
|
7
|
+
function normalizeString(value) {
|
|
8
|
+
return String(value || "").trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeUpper(value) {
|
|
12
|
+
return normalizeString(value).toUpperCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeTagList(value) {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const unique = new Set();
|
|
20
|
+
for (const item of value) {
|
|
21
|
+
const normalized = normalizeString(item);
|
|
22
|
+
if (normalized) {
|
|
23
|
+
unique.add(normalized);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...unique];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeLegalHoldStatus(value) {
|
|
30
|
+
const normalized = normalizeUpper(value);
|
|
31
|
+
if (!normalized) {
|
|
32
|
+
return "NONE";
|
|
33
|
+
}
|
|
34
|
+
if (normalized === "HOLD" || normalized === "NONE" || normalized === "UNKNOWN") {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeMetadata(value) {
|
|
41
|
+
const source = value && typeof value === "object" ? value : {};
|
|
42
|
+
return {
|
|
43
|
+
...source,
|
|
44
|
+
tags: normalizeTagList(source.tags),
|
|
45
|
+
legalHoldStatus: normalizeLegalHoldStatus(source.legalHoldStatus),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeIdentityRecord(record = {}) {
|
|
50
|
+
const metadata = normalizeMetadata(record.metadata);
|
|
51
|
+
const legalHoldStatus = normalizeLegalHoldStatus(record.legalHoldStatus || metadata.legalHoldStatus);
|
|
52
|
+
return {
|
|
53
|
+
identityId: normalizeString(record.identityId),
|
|
54
|
+
parentIdentityId: normalizeString(record.parentIdentityId) || null,
|
|
55
|
+
emailAddress: normalizeString(record.emailAddress) || null,
|
|
56
|
+
status: normalizeString(record.status) || "UNKNOWN",
|
|
57
|
+
projectId: normalizeString(record.projectId) || null,
|
|
58
|
+
orgId: normalizeString(record.orgId) || null,
|
|
59
|
+
apiUrl: normalizeString(record.apiUrl) || null,
|
|
60
|
+
createdAt: normalizeString(record.createdAt) || new Date().toISOString(),
|
|
61
|
+
lastUpdatedAt: normalizeString(record.lastUpdatedAt) || new Date().toISOString(),
|
|
62
|
+
expiresAt: normalizeString(record.expiresAt) || null,
|
|
63
|
+
revokedAt: normalizeString(record.revokedAt) || null,
|
|
64
|
+
squashedAt: normalizeString(record.squashedAt) || null,
|
|
65
|
+
legalHoldStatus,
|
|
66
|
+
metadata,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveIdentityRegistryPath({ outputRoot } = {}) {
|
|
71
|
+
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
72
|
+
return path.join(resolvedOutputRoot, "aidenid", "identity-registry.json");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadRegistryInternal(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
const raw = await fsp.readFile(filePath, "utf-8");
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
const identities = Array.isArray(parsed.identities)
|
|
80
|
+
? parsed.identities.map((item) => normalizeIdentityRecord(item)).filter((item) => item.identityId)
|
|
81
|
+
: [];
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: normalizeString(parsed.schemaVersion) || REGISTRY_SCHEMA_VERSION,
|
|
84
|
+
generatedAt: normalizeString(parsed.generatedAt) || new Date().toISOString(),
|
|
85
|
+
identities,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
91
|
+
generatedAt: new Date().toISOString(),
|
|
92
|
+
identities: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function writeRegistryInternal(filePath, registry = {}) {
|
|
100
|
+
const normalized = {
|
|
101
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
102
|
+
generatedAt: new Date().toISOString(),
|
|
103
|
+
identities: Array.isArray(registry.identities) ? registry.identities.map(normalizeIdentityRecord) : [],
|
|
104
|
+
};
|
|
105
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
106
|
+
await fsp.writeFile(filePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
|
|
107
|
+
return normalized;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function listIdentities({ outputRoot } = {}) {
|
|
111
|
+
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
112
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
113
|
+
return {
|
|
114
|
+
registryPath,
|
|
115
|
+
identities: registry.identities,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getIdentityById({ outputRoot, identityId } = {}) {
|
|
120
|
+
const normalizedIdentityId = normalizeString(identityId);
|
|
121
|
+
if (!normalizedIdentityId) {
|
|
122
|
+
throw new Error("identityId is required.");
|
|
123
|
+
}
|
|
124
|
+
const { registryPath, identities } = await listIdentities({ outputRoot });
|
|
125
|
+
const identity = identities.find((item) => item.identityId === normalizedIdentityId) || null;
|
|
126
|
+
return {
|
|
127
|
+
registryPath,
|
|
128
|
+
identity,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function recordProvisionedIdentity({
|
|
133
|
+
outputRoot,
|
|
134
|
+
response = {},
|
|
135
|
+
context = {},
|
|
136
|
+
} = {}) {
|
|
137
|
+
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
138
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
139
|
+
const identityId = normalizeString(response.id);
|
|
140
|
+
if (!identityId) {
|
|
141
|
+
throw new Error("Cannot record identity without response.id.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const nowIso = new Date().toISOString();
|
|
145
|
+
const source = normalizeString(context.source) || "provision-email";
|
|
146
|
+
const nextRecord = normalizeIdentityRecord({
|
|
147
|
+
identityId,
|
|
148
|
+
parentIdentityId: response.parentIdentityId || context.parentIdentityId || null,
|
|
149
|
+
emailAddress: response.emailAddress,
|
|
150
|
+
status: response.status || "ACTIVE",
|
|
151
|
+
projectId: response.projectId || context.projectId,
|
|
152
|
+
orgId: context.orgId,
|
|
153
|
+
apiUrl: context.apiUrl,
|
|
154
|
+
createdAt: nowIso,
|
|
155
|
+
lastUpdatedAt: nowIso,
|
|
156
|
+
expiresAt: response.expiresAt || null,
|
|
157
|
+
legalHoldStatus: response.legalHoldStatus || context.legalHoldStatus || "NONE",
|
|
158
|
+
metadata: {
|
|
159
|
+
source,
|
|
160
|
+
idempotencyKey: context.idempotencyKey || null,
|
|
161
|
+
eventBudget: context.eventBudget ?? null,
|
|
162
|
+
tags: normalizeTagList(context.tags || response.tags || []),
|
|
163
|
+
legalHoldStatus: response.legalHoldStatus || context.legalHoldStatus || "NONE",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const index = registry.identities.findIndex((item) => item.identityId === identityId);
|
|
168
|
+
if (index >= 0) {
|
|
169
|
+
const existing = registry.identities[index];
|
|
170
|
+
registry.identities[index] = normalizeIdentityRecord({
|
|
171
|
+
...existing,
|
|
172
|
+
...nextRecord,
|
|
173
|
+
createdAt: existing.createdAt || nextRecord.createdAt,
|
|
174
|
+
metadata: {
|
|
175
|
+
...existing.metadata,
|
|
176
|
+
...nextRecord.metadata,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
registry.identities.push(nextRecord);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
184
|
+
const identity = saved.identities.find((item) => item.identityId === identityId) || null;
|
|
185
|
+
return {
|
|
186
|
+
registryPath,
|
|
187
|
+
identity,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function updateIdentityStatus({
|
|
192
|
+
outputRoot,
|
|
193
|
+
identityId,
|
|
194
|
+
status,
|
|
195
|
+
revokedAt = "",
|
|
196
|
+
squashedAt = "",
|
|
197
|
+
metadataPatch = {},
|
|
198
|
+
} = {}) {
|
|
199
|
+
const normalizedIdentityId = normalizeString(identityId);
|
|
200
|
+
if (!normalizedIdentityId) {
|
|
201
|
+
throw new Error("identityId is required.");
|
|
202
|
+
}
|
|
203
|
+
const nextStatus = normalizeString(status);
|
|
204
|
+
if (!nextStatus) {
|
|
205
|
+
throw new Error("status is required.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const registryPath = resolveIdentityRegistryPath({ outputRoot });
|
|
209
|
+
const registry = await loadRegistryInternal(registryPath);
|
|
210
|
+
const index = registry.identities.findIndex((item) => item.identityId === normalizedIdentityId);
|
|
211
|
+
if (index < 0) {
|
|
212
|
+
throw new Error(`Identity '${normalizedIdentityId}' was not found in local registry.`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const existing = registry.identities[index];
|
|
216
|
+
registry.identities[index] = normalizeIdentityRecord({
|
|
217
|
+
...existing,
|
|
218
|
+
status: nextStatus,
|
|
219
|
+
revokedAt: normalizeString(revokedAt) || existing.revokedAt || null,
|
|
220
|
+
squashedAt: normalizeString(squashedAt) || existing.squashedAt || null,
|
|
221
|
+
legalHoldStatus: metadataPatch?.legalHoldStatus || existing.legalHoldStatus || "NONE",
|
|
222
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
223
|
+
metadata: {
|
|
224
|
+
...existing.metadata,
|
|
225
|
+
...(metadataPatch && typeof metadataPatch === "object" ? metadataPatch : {}),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const saved = await writeRegistryInternal(registryPath, registry);
|
|
230
|
+
return {
|
|
231
|
+
registryPath,
|
|
232
|
+
identity: saved.identities[index],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function filterIdentitiesByTags(identities = [], tags = []) {
|
|
237
|
+
const requestedTags = normalizeTagList(tags);
|
|
238
|
+
if (requestedTags.length === 0) {
|
|
239
|
+
return [...identities];
|
|
240
|
+
}
|
|
241
|
+
const required = new Set(requestedTags);
|
|
242
|
+
return identities.filter((identity) => {
|
|
243
|
+
const identityTags = normalizeTagList(identity?.metadata?.tags);
|
|
244
|
+
if (identityTags.length === 0) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return [...required].every((tag) => identityTags.includes(tag));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function findStaleIdentities({ outputRoot, nowIso = new Date().toISOString() } = {}) {
|
|
252
|
+
const { registryPath, identities } = await listIdentities({ outputRoot });
|
|
253
|
+
const nowMs = new Date(nowIso).getTime();
|
|
254
|
+
const stale = identities.filter((identity) => {
|
|
255
|
+
const status = normalizeUpper(identity.status);
|
|
256
|
+
if (TERMINAL_IDENTITY_STATUSES.has(status)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const expiresAtMs = new Date(identity.expiresAt || "").getTime();
|
|
260
|
+
if (!Number.isFinite(expiresAtMs)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return expiresAtMs <= nowMs;
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
registryPath,
|
|
267
|
+
identities,
|
|
268
|
+
stale,
|
|
269
|
+
};
|
|
270
|
+
}
|
package/src/ai/proxy.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SentinelLayer LLM proxy provider.
|
|
3
|
+
*
|
|
4
|
+
* Routes LLM calls through POST /api/v1/proxy/llm using the stored
|
|
5
|
+
* sentinelayer_token. Users never need their own OpenAI/Anthropic key.
|
|
6
|
+
*
|
|
7
|
+
* Request: { model, system_prompt, user_content, max_tokens, temperature }
|
|
8
|
+
* Response: { content, usage: { model, provider, tokens_in, tokens_out, cost_usd, latency_ms } }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
12
|
+
import { authLoginHint } from "../ui/command-hints.js";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PROXY_MODEL = "gpt-5.3-codex";
|
|
15
|
+
const PROXY_TIMEOUT_MS = 120_000;
|
|
16
|
+
const PROXY_MAX_RETRIES = 2;
|
|
17
|
+
const PROXY_RETRY_STATUSES = new Set([429, 502, 503, 504]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Invoke LLM via SentinelLayer proxy.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} options
|
|
23
|
+
* @param {string} options.prompt - The user content / prompt text
|
|
24
|
+
* @param {string} [options.systemPrompt] - System prompt
|
|
25
|
+
* @param {string} [options.model] - Model ID (default: gpt-5.3-codex)
|
|
26
|
+
* @param {number} [options.maxTokens] - Max output tokens (default: 4096)
|
|
27
|
+
* @param {number} [options.temperature] - Temperature (default: 0.1)
|
|
28
|
+
* @param {string} [options.apiUrl] - Override API URL
|
|
29
|
+
* @param {string} [options.token] - Override Bearer token
|
|
30
|
+
* @returns {Promise<{ text: string, usage: { inputTokens: number, outputTokens: number, costUsd: number, model: string, provider: string, latencyMs: number } }>}
|
|
31
|
+
*/
|
|
32
|
+
export async function invokeViaProxy({
|
|
33
|
+
prompt,
|
|
34
|
+
systemPrompt = "",
|
|
35
|
+
model = DEFAULT_PROXY_MODEL,
|
|
36
|
+
maxTokens = 4096,
|
|
37
|
+
temperature = 0.1,
|
|
38
|
+
apiUrl = "",
|
|
39
|
+
token = "",
|
|
40
|
+
} = {}) {
|
|
41
|
+
// Resolve credentials from session if not provided
|
|
42
|
+
let resolvedApiUrl = String(apiUrl || "").trim();
|
|
43
|
+
let resolvedToken = String(token || "").trim();
|
|
44
|
+
|
|
45
|
+
if (!resolvedApiUrl || !resolvedToken) {
|
|
46
|
+
const session = await resolveActiveAuthSession({
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
env: process.env,
|
|
49
|
+
autoRotate: false,
|
|
50
|
+
});
|
|
51
|
+
if (!session || !session.token) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`SentinelLayer LLM proxy requires authentication. Run '${authLoginHint()}' first.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!resolvedApiUrl) resolvedApiUrl = String(session.apiUrl || "https://api.sentinelayer.com").trim();
|
|
57
|
+
if (!resolvedToken) resolvedToken = String(session.token).trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const url = `${resolvedApiUrl.replace(/\/+$/, "")}/api/v1/proxy/llm`;
|
|
61
|
+
|
|
62
|
+
const body = JSON.stringify({
|
|
63
|
+
model,
|
|
64
|
+
system_prompt: systemPrompt || "You are a code reviewer.",
|
|
65
|
+
user_content: String(prompt || ""),
|
|
66
|
+
max_tokens: maxTokens,
|
|
67
|
+
temperature,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let response = null;
|
|
71
|
+
let lastError = null;
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt <= PROXY_MAX_RETRIES; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutHandle = setTimeout(() => controller.abort(), PROXY_TIMEOUT_MS);
|
|
77
|
+
try {
|
|
78
|
+
response = await fetch(url, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
Authorization: `Bearer ${resolvedToken}`,
|
|
83
|
+
Accept: "application/json",
|
|
84
|
+
},
|
|
85
|
+
body,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
} finally {
|
|
89
|
+
clearTimeout(timeoutHandle);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (PROXY_RETRY_STATUSES.has(response.status) && attempt < PROXY_MAX_RETRIES) {
|
|
93
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
94
|
+
const delay = retryAfter && !isNaN(retryAfter) ? Math.min(Number(retryAfter), 5) * 1000 : 500 * (attempt + 1);
|
|
95
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
break;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
lastError = err;
|
|
102
|
+
if (attempt >= PROXY_MAX_RETRIES) break;
|
|
103
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!response) {
|
|
108
|
+
throw new Error(`SentinelLayer LLM proxy request failed: ${lastError?.message || "no response"}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
let detail = "";
|
|
113
|
+
try {
|
|
114
|
+
const errBody = await response.json();
|
|
115
|
+
detail = errBody?.error?.message || errBody?.detail || JSON.stringify(errBody).slice(0, 200);
|
|
116
|
+
} catch {
|
|
117
|
+
detail = await response.text().catch(() => "");
|
|
118
|
+
}
|
|
119
|
+
throw new Error(`SentinelLayer LLM proxy error (${response.status}): ${detail}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = await response.json();
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
text: String(result.content || ""),
|
|
126
|
+
usage: {
|
|
127
|
+
inputTokens: result.usage?.tokens_in || 0,
|
|
128
|
+
outputTokens: result.usage?.tokens_out || 0,
|
|
129
|
+
costUsd: result.usage?.cost_usd || 0,
|
|
130
|
+
model: result.usage?.model || model,
|
|
131
|
+
provider: result.usage?.provider || "sentinelayer",
|
|
132
|
+
latencyMs: result.usage?.latency_ms || 0,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { DEFAULT_PROXY_MODEL };
|