Add more untracked runtime extension files
This commit is contained in:
parent
fd06629f06
commit
a46cbcbe40
10 changed files with 971 additions and 0 deletions
|
|
@ -0,0 +1,73 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
assessAssertionCoverage,
|
||||
fulfilledAssertionIdsFromHandoff,
|
||||
requiredAssertionIdsFromContract,
|
||||
} from "../uok/assertion-coverage.js";
|
||||
|
||||
test("requiredAssertionIdsFromContract_when_droid_and_sf_fields_present_returns_unique_required_ids", () => {
|
||||
assert.deepEqual(
|
||||
requiredAssertionIdsFromContract({
|
||||
assertionIds: ["VAL-001", "VAL-002"],
|
||||
fulfills: ["VAL-002", "VAL-003"],
|
||||
validationContract: {
|
||||
assertions: ["VAL-004"],
|
||||
fulfills: ["VAL-003"],
|
||||
},
|
||||
unitContract: {
|
||||
assertionIds: ["VAL-005"],
|
||||
},
|
||||
}),
|
||||
["VAL-001", "VAL-002", "VAL-003", "VAL-004", "VAL-005"],
|
||||
);
|
||||
});
|
||||
|
||||
test("fulfilledAssertionIdsFromHandoff_when_closeout_uses_alias_fields_returns_unique_fulfilled_ids", () => {
|
||||
assert.deepEqual(
|
||||
fulfilledAssertionIdsFromHandoff({
|
||||
fulfilledAssertions: ["VAL-001", "VAL-002"],
|
||||
assertionsFulfilled: ["VAL-002", "VAL-003"],
|
||||
fulfills: ["VAL-004"],
|
||||
}),
|
||||
["VAL-001", "VAL-002", "VAL-003", "VAL-004"],
|
||||
);
|
||||
});
|
||||
|
||||
test("assessAssertionCoverage_when_handoff_covers_all_required_assertions_returns_ok", () => {
|
||||
const result = assessAssertionCoverage(
|
||||
{
|
||||
fulfills: ["VAL-LOG-001", "VAL-LOG-002"],
|
||||
},
|
||||
{
|
||||
fulfilledAssertions: ["VAL-LOG-002", "VAL-LOG-001"],
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
ok: true,
|
||||
required: ["VAL-LOG-001", "VAL-LOG-002"],
|
||||
fulfilled: ["VAL-LOG-002", "VAL-LOG-001"],
|
||||
missing: [],
|
||||
extra: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("assessAssertionCoverage_when_handoff_omits_required_assertion_reports_missing_and_extra", () => {
|
||||
const result = assessAssertionCoverage(
|
||||
{
|
||||
validationContract: {
|
||||
assertionIds: ["VAL-A", "VAL-B"],
|
||||
},
|
||||
},
|
||||
{
|
||||
fulfilledAssertions: ["VAL-A", "VAL-C"],
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.required, ["VAL-A", "VAL-B"]);
|
||||
assert.deepEqual(result.fulfilled, ["VAL-A", "VAL-C"]);
|
||||
assert.deepEqual(result.missing, ["VAL-B"]);
|
||||
assert.deepEqual(result.extra, ["VAL-C"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
buildModelRouteSnapshot,
|
||||
redactModelConfigSecrets,
|
||||
sanitizeModelRouteSnapshot,
|
||||
} from "../uok/model-route-snapshot.js";
|
||||
|
||||
test("sanitizeModelRouteSnapshot_when_route_has_secret_fields_omits_credentials", () => {
|
||||
assert.deepEqual(
|
||||
sanitizeModelRouteSnapshot({
|
||||
provider: "zai",
|
||||
id: "glm-5.1",
|
||||
displayName: "Z.AI GLM",
|
||||
api: "openai-chat",
|
||||
authMode: "apiKey",
|
||||
baseUrl: "https://api.example.test/v1",
|
||||
apiKey: "secret-key",
|
||||
headers: { Authorization: "Bearer secret" },
|
||||
noImageSupport: true,
|
||||
}),
|
||||
{
|
||||
provider: "zai",
|
||||
model: "glm-5.1",
|
||||
displayName: "Z.AI GLM",
|
||||
api: "openai-chat",
|
||||
authMode: "apiKey",
|
||||
baseUrl: "https://api.example.test/v1",
|
||||
isCustom: false,
|
||||
byok: true,
|
||||
noImageSupport: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("redactModelConfigSecrets_when_config_contains_credentials_replaces_secret_values", () => {
|
||||
assert.deepEqual(
|
||||
redactModelConfigSecrets({
|
||||
provider: "kimi-coding",
|
||||
apiKey: "key-123",
|
||||
authHeader: "x-api-key",
|
||||
headers: { Authorization: "Bearer token" },
|
||||
baseUrl: "https://api.example.test/v1",
|
||||
}),
|
||||
{
|
||||
provider: "kimi-coding",
|
||||
apiKey: "[REDACTED]",
|
||||
authHeader: "[REDACTED]",
|
||||
headers: "[REDACTED]",
|
||||
baseUrl: "https://api.example.test/v1",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("buildModelRouteSnapshot_when_route_and_fallbacks_present_keeps_policy_auto_and_routes_safe", () => {
|
||||
const snapshot = buildModelRouteSnapshot({
|
||||
role: "worker",
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
route: {
|
||||
provider: "kimi-coding",
|
||||
model: "kimi-k2.6",
|
||||
authMode: "apiKey",
|
||||
apiKey: "secret",
|
||||
},
|
||||
routingReason: "auto selector",
|
||||
fallbacksTried: [
|
||||
{
|
||||
provider: "ollama-cloud",
|
||||
model: "deepseek-v4-pro",
|
||||
authMode: "apiKey",
|
||||
apiKey: "secret",
|
||||
},
|
||||
],
|
||||
configEvidence: {
|
||||
apiKey: "secret",
|
||||
baseUrl: "https://api.example.test/v1",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(snapshot.schemaVersion, 1);
|
||||
assert.deepEqual(snapshot.requestedPolicy, { mode: "auto", constraints: [] });
|
||||
assert.equal(snapshot.route.provider, "kimi-coding");
|
||||
assert.equal(snapshot.route.model, "kimi-k2.6");
|
||||
assert.equal(snapshot.route.byok, true);
|
||||
assert.equal(snapshot.route.apiKey, undefined);
|
||||
assert.equal(snapshot.fallbacksTried[0].provider, "ollama-cloud");
|
||||
assert.equal(snapshot.fallbacksTried[0].apiKey, undefined);
|
||||
assert.equal(snapshot.configEvidence.apiKey, "[REDACTED]");
|
||||
assert.equal(snapshot.configEvidence.baseUrl, "https://api.example.test/v1");
|
||||
});
|
||||
68
src/resources/extensions/sf/tests/progress-event.test.mjs
Normal file
68
src/resources/extensions/sf/tests/progress-event.test.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
buildUokProgressEvent,
|
||||
UOK_PROGRESS_EVENT_TYPES,
|
||||
validateUokProgressEvent,
|
||||
} from "../uok/progress-event.js";
|
||||
|
||||
test("buildUokProgressEvent_when_unit_selected_returns_stable_machine_event", () => {
|
||||
assert.deepEqual(
|
||||
buildUokProgressEvent({
|
||||
ts: "2026-05-08T17:00:00.000Z",
|
||||
eventType: "unit_selected",
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
role: "worker",
|
||||
sessionId: "session-1",
|
||||
workerSessionId: "worker-1",
|
||||
traceId: "trace-1",
|
||||
data: { source: "dispatch" },
|
||||
}),
|
||||
{
|
||||
schemaVersion: 1,
|
||||
ts: "2026-05-08T17:00:00.000Z",
|
||||
eventType: "unit_selected",
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
role: "worker",
|
||||
sessionId: "session-1",
|
||||
workerSessionId: "worker-1",
|
||||
traceId: "trace-1",
|
||||
data: { source: "dispatch" },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("validateUokProgressEvent_when_event_type_unknown_reports_allowed_types", () => {
|
||||
const result = validateUokProgressEvent({
|
||||
eventType: "invented_event",
|
||||
data: {},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.issues[0], /eventType must be one of/);
|
||||
assert.match(result.issues[0], /unit_selected/);
|
||||
assert.match(result.issues[0], /unit_handoff_recorded/);
|
||||
});
|
||||
|
||||
test("buildUokProgressEvent_when_payload_data_is_not_object_rejects_event", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
buildUokProgressEvent({
|
||||
eventType: "worker_started",
|
||||
data: "not-object",
|
||||
}),
|
||||
/Invalid UOK progress event: data must be an object/,
|
||||
);
|
||||
});
|
||||
|
||||
test("progressEventTypes_include_droid_value_events_without_concrete_model_config", () => {
|
||||
assert.ok(UOK_PROGRESS_EVENT_TYPES.includes("model_auto_resolved"));
|
||||
assert.ok(UOK_PROGRESS_EVENT_TYPES.includes("validation_started"));
|
||||
assert.ok(UOK_PROGRESS_EVENT_TYPES.includes("unit_handoff_recorded"));
|
||||
assert.equal(
|
||||
UOK_PROGRESS_EVENT_TYPES.some((type) => type.includes("custom:")),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
normalizeToolCommandRegistry,
|
||||
validateToolCommandRegistry,
|
||||
} from "../uok/tool-command-registry.js";
|
||||
|
||||
test("normalizeToolCommandRegistry_when_commands_and_daemon_services_present_returns_worker_tool_allowlist", () => {
|
||||
assert.deepEqual(
|
||||
normalizeToolCommandRegistry({
|
||||
commands: {
|
||||
test: " npm run test:unit ",
|
||||
typecheck: "npm run typecheck:extensions",
|
||||
},
|
||||
daemonServices: [" postgres ", "redis", "redis"],
|
||||
}),
|
||||
{
|
||||
schemaVersion: 1,
|
||||
commands: {
|
||||
test: "npm run test:unit",
|
||||
typecheck: "npm run typecheck:extensions",
|
||||
},
|
||||
daemonServices: ["postgres", "redis"],
|
||||
allowedCommands: ["npm run test:unit", "npm run typecheck:extensions"],
|
||||
allowedCommandNames: ["test", "typecheck"],
|
||||
allowedDaemonServices: ["postgres", "redis"],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeToolCommandRegistry_when_fields_missing_returns_empty_allowlists", () => {
|
||||
assert.deepEqual(normalizeToolCommandRegistry({}), {
|
||||
schemaVersion: 1,
|
||||
commands: {},
|
||||
daemonServices: [],
|
||||
allowedCommands: [],
|
||||
allowedCommandNames: [],
|
||||
allowedDaemonServices: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("validateToolCommandRegistry_when_commands_not_object_reports_clear_issue", () => {
|
||||
const result = validateToolCommandRegistry({
|
||||
commands: ["npm test"],
|
||||
daemonServices: [],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.issues, ["commands must be an object"]);
|
||||
});
|
||||
|
||||
test("validateToolCommandRegistry_when_command_or_daemon_service_empty_reports_paths", () => {
|
||||
const result = validateToolCommandRegistry({
|
||||
commands: {
|
||||
test: "",
|
||||
},
|
||||
daemonServices: ["postgres", ""],
|
||||
});
|
||||
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.issues, [
|
||||
"commands.test must be a non-empty string",
|
||||
"daemonServices[1] must be a non-empty string",
|
||||
]);
|
||||
});
|
||||
|
||||
test("normalizeToolCommandRegistry_when_droid_services_field_present_treats_it_as_legacy_daemon_services", () => {
|
||||
assert.deepEqual(
|
||||
normalizeToolCommandRegistry({
|
||||
commands: {
|
||||
build: "npm run build:core",
|
||||
},
|
||||
services: ["postgres"],
|
||||
}),
|
||||
{
|
||||
schemaVersion: 1,
|
||||
commands: {
|
||||
build: "npm run build:core",
|
||||
},
|
||||
daemonServices: ["postgres"],
|
||||
allowedCommands: ["npm run build:core"],
|
||||
allowedCommandNames: ["build"],
|
||||
allowedDaemonServices: ["postgres"],
|
||||
},
|
||||
);
|
||||
});
|
||||
105
src/resources/extensions/sf/tests/unit-lineage.test.mjs
Normal file
105
src/resources/extensions/sf/tests/unit-lineage.test.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
normalizeUnitLineage,
|
||||
recordUnitLineageEvent,
|
||||
} from "../uok/unit-lineage.js";
|
||||
|
||||
test("normalizeUnitLineage_when_worker_ids_repeat_returns_deduplicated_lineage", () => {
|
||||
assert.deepEqual(
|
||||
normalizeUnitLineage({
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
workerSessionIds: ["worker-1", "worker-1"],
|
||||
currentWorkerSessionId: "worker-2",
|
||||
completedWorkerSessionId: "worker-3",
|
||||
failedWorkerSessionIds: ["worker-4", "worker-4"],
|
||||
status: "started",
|
||||
}),
|
||||
{
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
status: "started",
|
||||
workerSessionIds: ["worker-1", "worker-2", "worker-3", "worker-4"],
|
||||
currentWorkerSessionId: "worker-2",
|
||||
completedWorkerSessionId: "worker-3",
|
||||
failedWorkerSessionIds: ["worker-4"],
|
||||
events: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("recordUnitLineageEvent_when_worker_starts_marks_current_session", () => {
|
||||
const lineage = recordUnitLineageEvent(
|
||||
{ unitType: "execute-task", unitId: "M001/S01/T01" },
|
||||
{
|
||||
ts: "2026-05-08T17:00:00.000Z",
|
||||
status: "started",
|
||||
workerSessionId: "worker-1",
|
||||
spawnId: "spawn-1",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(lineage.status, "started");
|
||||
assert.deepEqual(lineage.workerSessionIds, ["worker-1"]);
|
||||
assert.equal(lineage.currentWorkerSessionId, "worker-1");
|
||||
assert.equal(lineage.events[0].spawnId, "spawn-1");
|
||||
});
|
||||
|
||||
test("recordUnitLineageEvent_when_worker_completes_moves_current_to_completed", () => {
|
||||
const started = recordUnitLineageEvent(
|
||||
{},
|
||||
{
|
||||
status: "started",
|
||||
workerSessionId: "worker-1",
|
||||
},
|
||||
);
|
||||
const completed = recordUnitLineageEvent(started, {
|
||||
status: "completed",
|
||||
workerSessionId: "worker-1",
|
||||
});
|
||||
|
||||
assert.equal(completed.status, "completed");
|
||||
assert.equal(completed.currentWorkerSessionId, null);
|
||||
assert.equal(completed.completedWorkerSessionId, "worker-1");
|
||||
assert.deepEqual(completed.failedWorkerSessionIds, []);
|
||||
});
|
||||
|
||||
test("recordUnitLineageEvent_when_worker_fails_tracks_failed_session", () => {
|
||||
const lineage = recordUnitLineageEvent(
|
||||
{
|
||||
workerSessionIds: ["worker-1"],
|
||||
currentWorkerSessionId: "worker-1",
|
||||
},
|
||||
{
|
||||
status: "failed",
|
||||
workerSessionId: "worker-1",
|
||||
note: "provider timeout",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(lineage.status, "failed");
|
||||
assert.equal(lineage.currentWorkerSessionId, null);
|
||||
assert.deepEqual(lineage.failedWorkerSessionIds, ["worker-1"]);
|
||||
assert.equal(lineage.events[0].note, "provider timeout");
|
||||
});
|
||||
|
||||
test("recordUnitLineageEvent_when_unit_blocks_clears_current_worker", () => {
|
||||
const lineage = recordUnitLineageEvent(
|
||||
{
|
||||
unitType: "execute-task",
|
||||
unitId: "M001/S01/T01",
|
||||
workerSessionIds: ["worker-1"],
|
||||
currentWorkerSessionId: "worker-1",
|
||||
},
|
||||
{
|
||||
status: "blocked",
|
||||
workerSessionId: "worker-1",
|
||||
note: "artifact verification failed",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(lineage.status, "blocked");
|
||||
assert.equal(lineage.currentWorkerSessionId, null);
|
||||
assert.deepEqual(lineage.workerSessionIds, ["worker-1"]);
|
||||
});
|
||||
103
src/resources/extensions/sf/uok/assertion-coverage.js
Normal file
103
src/resources/extensions/sf/uok/assertion-coverage.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* assertion-coverage.js - validation assertion coverage helpers.
|
||||
*
|
||||
* Purpose: preserve Droid's useful `fulfills: ["VAL-*"]` accounting as
|
||||
* structured SF data so units can prove which validation claims they satisfy.
|
||||
*
|
||||
* Consumer: autonomous unit contract projections, handoff validation, and
|
||||
* future closeout gates before marking a unit complete.
|
||||
*/
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
? value.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function stringList(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => stringOrNull(item))
|
||||
.filter((item) => item !== null);
|
||||
}
|
||||
const single = stringOrNull(value);
|
||||
return single === null ? [] : [single];
|
||||
}
|
||||
|
||||
function uniqueIds(...values) {
|
||||
return [...new Set(values.flatMap((value) => stringList(value)))];
|
||||
}
|
||||
|
||||
function objectOrEmpty(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize assertion IDs required by a unit contract.
|
||||
*
|
||||
* Purpose: let SF accept Droid-style `fulfills` as well as SF-style
|
||||
* `assertionIds` while projecting one canonical required assertion list.
|
||||
*
|
||||
* Consumer: assertion coverage checks and worker context-pack builders.
|
||||
*/
|
||||
export function requiredAssertionIdsFromContract(contract = {}) {
|
||||
const source = objectOrEmpty(contract);
|
||||
const validationContract = objectOrEmpty(source.validationContract);
|
||||
const unitContract = objectOrEmpty(source.unitContract);
|
||||
return uniqueIds(
|
||||
source.assertions,
|
||||
source.assertionIds,
|
||||
source.fulfills,
|
||||
source.fulfillmentIds,
|
||||
validationContract.assertions,
|
||||
validationContract.assertionIds,
|
||||
validationContract.fulfills,
|
||||
unitContract.assertions,
|
||||
unitContract.assertionIds,
|
||||
unitContract.fulfills,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize assertion IDs fulfilled by a unit handoff.
|
||||
*
|
||||
* Purpose: give closeout gates one canonical fulfilled assertion list even
|
||||
* when agents use Droid-style or SF-style field names.
|
||||
*
|
||||
* Consumer: assertion coverage checks and autonomous closeout validation.
|
||||
*/
|
||||
export function fulfilledAssertionIdsFromHandoff(handoff = {}) {
|
||||
const source = objectOrEmpty(handoff);
|
||||
return uniqueIds(
|
||||
source.fulfilledAssertions,
|
||||
source.assertionsFulfilled,
|
||||
source.fulfills,
|
||||
source.assertionIds,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a unit contract with a worker handoff and report assertion coverage.
|
||||
*
|
||||
* Purpose: prevent units from being treated as complete when their structured
|
||||
* handoff did not cover every validation claim the unit contract required.
|
||||
*
|
||||
* Consumer: future autonomous closeout gates and tests that need deterministic
|
||||
* validation coverage evidence without parsing prose.
|
||||
*/
|
||||
export function assessAssertionCoverage(contract = {}, handoff = {}) {
|
||||
const required = requiredAssertionIdsFromContract(contract);
|
||||
const fulfilled = fulfilledAssertionIdsFromHandoff(handoff);
|
||||
const fulfilledSet = new Set(fulfilled);
|
||||
const missing = required.filter((id) => !fulfilledSet.has(id));
|
||||
const extra = fulfilled.filter((id) => !required.includes(id));
|
||||
return {
|
||||
ok: missing.length === 0,
|
||||
required,
|
||||
fulfilled,
|
||||
missing,
|
||||
extra,
|
||||
};
|
||||
}
|
||||
97
src/resources/extensions/sf/uok/model-route-snapshot.js
Normal file
97
src/resources/extensions/sf/uok/model-route-snapshot.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* model-route-snapshot.js - secret-safe model route snapshots.
|
||||
*
|
||||
* Purpose: keep Droid's useful runtime model reproducibility evidence while
|
||||
* guaranteeing API keys, tokens, and headers are never persisted in SF state.
|
||||
*
|
||||
* Consumer: UOK audit/progress projections and future run-state snapshots that
|
||||
* need provider/model evidence without copying credentials.
|
||||
*/
|
||||
|
||||
const SECRET_KEYS = new Set([
|
||||
"apiKey",
|
||||
"api_key",
|
||||
"authorization",
|
||||
"authHeader",
|
||||
"headers",
|
||||
"token",
|
||||
"secret",
|
||||
"password",
|
||||
]);
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
? value.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function maybeRedacted(value, key) {
|
||||
if (SECRET_KEYS.has(key)) return "[REDACTED]";
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a secret-safe snapshot of one resolved model route.
|
||||
*
|
||||
* Purpose: record what SF actually routed to while keeping durable evidence
|
||||
* separate from BYOK credentials and provider secrets.
|
||||
*
|
||||
* Consumer: model route evidence emitters and run-state projection writers.
|
||||
*/
|
||||
export function sanitizeModelRouteSnapshot(route = {}) {
|
||||
const source = route && typeof route === "object" ? route : {};
|
||||
return {
|
||||
provider: stringOrNull(source.provider),
|
||||
model: stringOrNull(source.id ?? source.model ?? source.modelId),
|
||||
displayName: stringOrNull(source.displayName),
|
||||
api: stringOrNull(source.api),
|
||||
authMode: stringOrNull(source.authMode),
|
||||
baseUrl: stringOrNull(source.baseUrl),
|
||||
isCustom: Boolean(source.isCustom ?? source.custom),
|
||||
byok: source.authMode === "apiKey" || Boolean(source.byok),
|
||||
noImageSupport: Boolean(source.noImageSupport),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Redact a provider/model configuration object while preserving non-secret
|
||||
* route metadata.
|
||||
*
|
||||
* Purpose: make tests and projections fail closed if a caller accidentally
|
||||
* passes Droid-style custom model config containing raw credentials.
|
||||
*
|
||||
* Consumer: snapshot writers before serializing model/provider config evidence.
|
||||
*/
|
||||
export function redactModelConfigSecrets(config = {}) {
|
||||
if (!config || typeof config !== "object" || Array.isArray(config)) return {};
|
||||
const redacted = {};
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
redacted[key] = maybeRedacted(value, key);
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a secret-safe snapshot for a route plus optional routing evidence.
|
||||
*
|
||||
* Purpose: capture enough model evidence to debug auto routing decisions
|
||||
* without making concrete model IDs durable policy or leaking BYOK secrets.
|
||||
*
|
||||
* Consumer: UOK model routing events and future run-state projections.
|
||||
*/
|
||||
export function buildModelRouteSnapshot(args = {}) {
|
||||
const route = sanitizeModelRouteSnapshot(args.route ?? args.model ?? {});
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
role: stringOrNull(args.role),
|
||||
unitType: stringOrNull(args.unitType),
|
||||
unitId: stringOrNull(args.unitId),
|
||||
requestedPolicy: args.requestedPolicy ?? { mode: "auto", constraints: [] },
|
||||
route,
|
||||
routingReason: stringOrNull(args.routingReason),
|
||||
fallbacksTried: Array.isArray(args.fallbacksTried)
|
||||
? args.fallbacksTried.map(sanitizeModelRouteSnapshot)
|
||||
: [],
|
||||
configEvidence: redactModelConfigSecrets(args.configEvidence ?? {}),
|
||||
};
|
||||
}
|
||||
109
src/resources/extensions/sf/uok/progress-event.js
Normal file
109
src/resources/extensions/sf/uok/progress-event.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* progress-event.js - typed autonomous progress event helpers.
|
||||
*
|
||||
* Purpose: keep Droid's useful machine-readable progress vocabulary while
|
||||
* preserving SF's existing journal/audit systems as the transport.
|
||||
*
|
||||
* Consumer: autonomous loop projections, dashboards, and future machine
|
||||
* surfaces that need stable progress event names instead of parsing prose.
|
||||
*/
|
||||
|
||||
export const UOK_PROGRESS_EVENT_TYPES = Object.freeze([
|
||||
"unit_selected",
|
||||
"model_auto_resolved",
|
||||
"worker_started",
|
||||
"validation_started",
|
||||
"unit_handoff_recorded",
|
||||
"unit_blocked",
|
||||
"unit_completed",
|
||||
]);
|
||||
|
||||
const EVENT_TYPES = new Set(UOK_PROGRESS_EVENT_TYPES);
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
? value.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function objectOrEmpty(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a typed UOK progress event.
|
||||
*
|
||||
* Purpose: reject unstable ad hoc event names before they become dashboard or
|
||||
* machine-surface contracts.
|
||||
*
|
||||
* Consumer: buildUokProgressEvent and tests that need non-throwing diagnostics.
|
||||
*/
|
||||
export function validateUokProgressEvent(event) {
|
||||
const issues = [];
|
||||
if (!event || typeof event !== "object" || Array.isArray(event)) {
|
||||
return { ok: false, issues: ["event must be an object"] };
|
||||
}
|
||||
if (!EVENT_TYPES.has(event.eventType)) {
|
||||
issues.push(
|
||||
`eventType must be one of ${UOK_PROGRESS_EVENT_TYPES.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
"unitId" in event &&
|
||||
event.unitId !== null &&
|
||||
typeof event.unitId !== "string"
|
||||
) {
|
||||
issues.push("unitId must be a string or null");
|
||||
}
|
||||
if (
|
||||
"unitType" in event &&
|
||||
event.unitType !== null &&
|
||||
typeof event.unitType !== "string"
|
||||
) {
|
||||
issues.push("unitType must be a string or null");
|
||||
}
|
||||
if (
|
||||
"data" in event &&
|
||||
(typeof event.data !== "object" ||
|
||||
event.data === null ||
|
||||
Array.isArray(event.data))
|
||||
) {
|
||||
issues.push("data must be an object");
|
||||
}
|
||||
return { ok: issues.length === 0, issues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a typed UOK progress event with canonical timestamp and payload shape.
|
||||
*
|
||||
* Purpose: give SF the useful Droid-style progress stream shape without
|
||||
* coupling event creation to a specific file, DB, or transport.
|
||||
*
|
||||
* Consumer: future autonomous journal/audit adapters and focused tests.
|
||||
*/
|
||||
export function buildUokProgressEvent(args = {}) {
|
||||
const event = {
|
||||
schemaVersion: 1,
|
||||
ts: args.ts ?? new Date().toISOString(),
|
||||
eventType: args.eventType,
|
||||
unitType: stringOrNull(args.unitType),
|
||||
unitId: stringOrNull(args.unitId),
|
||||
role: stringOrNull(args.role),
|
||||
sessionId: stringOrNull(args.sessionId),
|
||||
workerSessionId: stringOrNull(args.workerSessionId),
|
||||
traceId: stringOrNull(args.traceId),
|
||||
data: args.data === undefined ? {} : args.data,
|
||||
};
|
||||
const validation = validateUokProgressEvent(event);
|
||||
if (!validation.ok) {
|
||||
throw new TypeError(
|
||||
`Invalid UOK progress event: ${validation.issues.join("; ")}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...event,
|
||||
data: objectOrEmpty(event.data),
|
||||
};
|
||||
}
|
||||
117
src/resources/extensions/sf/uok/tool-command-registry.js
Normal file
117
src/resources/extensions/sf/uok/tool-command-registry.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* tool-command-registry.js - worker tool/command registry helpers.
|
||||
*
|
||||
* Purpose: preserve Droid's useful command boundary as structured SF data so
|
||||
* workers receive explicit repo tools/commands instead of free-form prose.
|
||||
*
|
||||
* Consumer: worker context pack projections and future dispatch prompts that
|
||||
* need a tool-command allowlist without treating ad hoc mission files as
|
||||
* canonical.
|
||||
*/
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
? value.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function objectOrEmpty(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value
|
||||
: {};
|
||||
}
|
||||
|
||||
function normalizeStringList(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return [
|
||||
...new Set(value.map(stringOrNull).filter((item) => item !== null)),
|
||||
];
|
||||
}
|
||||
const single = stringOrNull(value);
|
||||
return single === null ? [] : [single];
|
||||
}
|
||||
|
||||
function normalizeCommandMap(commands) {
|
||||
const source = objectOrEmpty(commands);
|
||||
const normalized = {};
|
||||
for (const [name, command] of Object.entries(source)) {
|
||||
const normalizedName = stringOrNull(name);
|
||||
const normalizedCommand = stringOrNull(command);
|
||||
if (normalizedName && normalizedCommand) {
|
||||
normalized[normalizedName] = normalizedCommand;
|
||||
}
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an SF worker tool/command registry.
|
||||
*
|
||||
* Purpose: project Droid-style command boundaries into a predictable
|
||||
* tool-first shape while leaving durable source-of-truth ownership to SF
|
||||
* DB/runtime state.
|
||||
*
|
||||
* Consumer: context-pack builders and prompt adapters before rendering worker
|
||||
* instructions.
|
||||
*/
|
||||
export function normalizeToolCommandRegistry(registry = {}) {
|
||||
const source = objectOrEmpty(registry);
|
||||
const commands = normalizeCommandMap(source.commands);
|
||||
const daemonServices = normalizeStringList(
|
||||
source.daemonServices ?? source.services,
|
||||
);
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
commands,
|
||||
daemonServices,
|
||||
allowedCommands: Object.values(commands),
|
||||
allowedCommandNames: Object.keys(commands),
|
||||
allowedDaemonServices: daemonServices,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a worker tool/command registry.
|
||||
*
|
||||
* Purpose: reject malformed command boundaries before workers treat them as
|
||||
* executable instructions.
|
||||
*
|
||||
* Consumer: context-pack projection validation and tests.
|
||||
*/
|
||||
export function validateToolCommandRegistry(registry) {
|
||||
const issues = [];
|
||||
if (!registry || typeof registry !== "object" || Array.isArray(registry)) {
|
||||
return { ok: false, issues: ["registry must be an object"] };
|
||||
}
|
||||
if ("commands" in registry) {
|
||||
if (
|
||||
!registry.commands ||
|
||||
typeof registry.commands !== "object" ||
|
||||
Array.isArray(registry.commands)
|
||||
) {
|
||||
issues.push("commands must be an object");
|
||||
} else {
|
||||
for (const [name, command] of Object.entries(registry.commands)) {
|
||||
if (!stringOrNull(name)) issues.push("command name must be non-empty");
|
||||
if (!stringOrNull(command)) {
|
||||
issues.push(`commands.${name} must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const servicesValue = registry.daemonServices ?? registry.services;
|
||||
if (servicesValue !== undefined && !Array.isArray(servicesValue)) {
|
||||
issues.push("daemonServices must be an array");
|
||||
}
|
||||
if (Array.isArray(servicesValue)) {
|
||||
servicesValue.forEach((service, index) => {
|
||||
if (!stringOrNull(service)) {
|
||||
issues.push(`daemonServices[${index}] must be a non-empty string`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return { ok: issues.length === 0, issues };
|
||||
}
|
||||
|
||||
export const normalizeCommandRegistry = normalizeToolCommandRegistry;
|
||||
export const validateCommandRegistry = validateToolCommandRegistry;
|
||||
122
src/resources/extensions/sf/uok/unit-lineage.js
Normal file
122
src/resources/extensions/sf/uok/unit-lineage.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* unit-lineage.js - autonomous unit worker/session lineage helpers.
|
||||
*
|
||||
* Purpose: preserve Droid's useful worker-session accounting so SF can explain
|
||||
* which sessions claimed, ran, completed, or failed a unit.
|
||||
*
|
||||
* Consumer: autonomous dispatch projections, progress events, and future
|
||||
* recovery screens that need lineage without parsing logs.
|
||||
*/
|
||||
|
||||
const LINEAGE_STATUSES = new Set([
|
||||
"selected",
|
||||
"started",
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
"blocked",
|
||||
]);
|
||||
|
||||
function stringOrNull(value) {
|
||||
return typeof value === "string" && value.trim().length > 0
|
||||
? value.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
function uniqueStringList(value) {
|
||||
const values = Array.isArray(value) ? value : [];
|
||||
return [...new Set(values.map(stringOrNull).filter((item) => item !== null))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a unit lineage record.
|
||||
*
|
||||
* Purpose: keep worker-session lineage deterministic and deduplicated before
|
||||
* it is projected into runtime state or machine progress events.
|
||||
*
|
||||
* Consumer: recordUnitLineageEvent and future dispatch projection writers.
|
||||
*/
|
||||
export function normalizeUnitLineage(record = {}) {
|
||||
const workerSessionIds = uniqueStringList(record.workerSessionIds);
|
||||
const currentWorkerSessionId = stringOrNull(record.currentWorkerSessionId);
|
||||
const completedWorkerSessionId = stringOrNull(
|
||||
record.completedWorkerSessionId,
|
||||
);
|
||||
const failedWorkerSessionIds = uniqueStringList(
|
||||
record.failedWorkerSessionIds,
|
||||
);
|
||||
if (
|
||||
currentWorkerSessionId &&
|
||||
!workerSessionIds.includes(currentWorkerSessionId)
|
||||
) {
|
||||
workerSessionIds.push(currentWorkerSessionId);
|
||||
}
|
||||
if (
|
||||
completedWorkerSessionId &&
|
||||
!workerSessionIds.includes(completedWorkerSessionId)
|
||||
) {
|
||||
workerSessionIds.push(completedWorkerSessionId);
|
||||
}
|
||||
for (const id of failedWorkerSessionIds) {
|
||||
if (!workerSessionIds.includes(id)) workerSessionIds.push(id);
|
||||
}
|
||||
return {
|
||||
unitType: stringOrNull(record.unitType),
|
||||
unitId: stringOrNull(record.unitId),
|
||||
status: LINEAGE_STATUSES.has(record.status) ? record.status : "selected",
|
||||
workerSessionIds,
|
||||
currentWorkerSessionId,
|
||||
completedWorkerSessionId,
|
||||
failedWorkerSessionIds,
|
||||
events: Array.isArray(record.events) ? record.events : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one worker/session lineage event to a unit lineage record.
|
||||
*
|
||||
* Purpose: model Droid-style worker lifecycle accounting as structured state
|
||||
* while leaving persistence and transport to SF's existing DB/journal layers.
|
||||
*
|
||||
* Consumer: future autonomous dispatch hooks and tests.
|
||||
*/
|
||||
export function recordUnitLineageEvent(record = {}, event = {}) {
|
||||
const current = normalizeUnitLineage(record);
|
||||
const status = LINEAGE_STATUSES.has(event.status) ? event.status : "selected";
|
||||
const workerSessionId = stringOrNull(event.workerSessionId);
|
||||
const next = {
|
||||
...current,
|
||||
unitType: stringOrNull(event.unitType) ?? current.unitType,
|
||||
unitId: stringOrNull(event.unitId) ?? current.unitId,
|
||||
status,
|
||||
events: [
|
||||
...current.events,
|
||||
{
|
||||
ts: event.ts ?? new Date().toISOString(),
|
||||
status,
|
||||
workerSessionId,
|
||||
spawnId: stringOrNull(event.spawnId),
|
||||
note: stringOrNull(event.note),
|
||||
},
|
||||
],
|
||||
};
|
||||
if (workerSessionId && !next.workerSessionIds.includes(workerSessionId)) {
|
||||
next.workerSessionIds.push(workerSessionId);
|
||||
}
|
||||
if (status === "started") next.currentWorkerSessionId = workerSessionId;
|
||||
if (status === "completed") {
|
||||
next.completedWorkerSessionId = workerSessionId;
|
||||
next.currentWorkerSessionId = null;
|
||||
}
|
||||
if (status === "failed" && workerSessionId) {
|
||||
next.failedWorkerSessionIds = uniqueStringList([
|
||||
...next.failedWorkerSessionIds,
|
||||
workerSessionId,
|
||||
]);
|
||||
next.currentWorkerSessionId = null;
|
||||
}
|
||||
if (status === "blocked" || status === "cancelled") {
|
||||
next.currentWorkerSessionId = null;
|
||||
}
|
||||
return normalizeUnitLineage(next);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue