@intx/hub-sessions 0.2.2 → 0.4.0
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 +3 -5
- package/dist/agent-repo.d.ts +23 -7
- package/dist/agent-repo.js +19 -6
- package/dist/agent-state-kind.js +18 -63
- package/dist/asset-service.d.ts +1 -20
- package/dist/asset-service.js +19 -97
- package/dist/committed-source-tree.d.ts +10 -0
- package/dist/committed-source-tree.js +35 -0
- package/dist/credential-push.d.ts +52 -7
- package/dist/credential-push.js +178 -22
- package/dist/event-collector-registry.d.ts +3 -2
- package/dist/event-collector-registry.js +42 -13
- package/dist/event-collector.d.ts +12 -2
- package/dist/event-collector.js +45 -4
- package/dist/hub-session-lookups.d.ts +125 -7
- package/dist/hub-session-lookups.js +541 -86
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +22 -56
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +2 -1
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/store.d.ts +1 -1
- package/dist/repo-store/store.js +138 -1
- package/dist/repo-store/subscribe-kind.d.ts +6 -3
- package/dist/repo-store/subscribe-kind.js +42 -77
- package/dist/repo-store/types.d.ts +94 -6
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +252 -126
- package/dist/session-service.js +851 -624
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +101 -0
- package/dist/sidecar-allocation/contracts.js +26 -0
- package/dist/sidecar-allocation/index.d.ts +5 -0
- package/dist/sidecar-allocation/index.js +4 -0
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +24 -0
- package/dist/sidecar-allocation/plugin-registry.js +61 -0
- package/dist/sidecar-allocation/reconciler.d.ts +54 -0
- package/dist/sidecar-allocation/reconciler.js +825 -0
- package/dist/skill-kind.js +12 -62
- package/dist/substrate.d.ts +3 -3
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +64 -0
- package/dist/workflow-allocation-service.js +554 -0
- package/dist/workflow-closure-resolution.d.ts +106 -0
- package/dist/workflow-closure-resolution.js +123 -0
- package/dist/workflow-definition-ensure.d.ts +24 -0
- package/dist/workflow-definition-ensure.js +75 -0
- package/dist/workflow-dispatch-service.d.ts +42 -0
- package/dist/workflow-dispatch-service.js +209 -0
- package/dist/workflow-dispatch-settlement.d.ts +29 -0
- package/dist/workflow-dispatch-settlement.js +140 -0
- package/dist/workflow-kind.d.ts +29 -1
- package/dist/workflow-kind.js +143 -139
- package/dist/workflow-probe-gate.d.ts +286 -0
- package/dist/workflow-probe-gate.js +382 -0
- package/dist/workflow-run-kind.d.ts +239 -32
- package/dist/workflow-run-kind.js +874 -188
- package/dist/workflow-run-reader.d.ts +1 -1
- package/dist/workflow-run-reader.js +3 -7
- package/dist/workflow-run-restore.d.ts +16 -0
- package/dist/workflow-run-restore.js +30 -0
- package/dist/workflow-source-closure.d.ts +35 -0
- package/dist/workflow-source-closure.js +342 -0
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +3 -3
- package/dist/ws/index.js +2 -2
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +139 -37
- package/dist/ws/sidecar-events.js +2 -2
- package/dist/ws/sidecar-handler.d.ts +218 -60
- package/dist/ws/sidecar-handler.js +1713 -732
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.d.ts +3 -1
- package/dist/ws/sidecar-token-authenticator.js +78 -7
- package/package.json +14 -13
- package/dist/available-skills-stanza.d.ts +0 -21
- package/dist/available-skills-stanza.js +0 -32
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HarnessConfig } from "@intx/types/runtime";
|
|
2
|
+
import { createSidecarRouter, type AllocatedSidecarTarget, type SidecarAuthIdentity, type SidecarRouterConfig, type WsHandle } from "./sidecar-handler.js";
|
|
3
|
+
export declare const TEST_IDENTITY: Extract<SidecarAuthIdentity, {
|
|
4
|
+
kind: "allocated";
|
|
5
|
+
}>;
|
|
6
|
+
export declare const TEST_TARGET: AllocatedSidecarTarget;
|
|
7
|
+
export declare const TEST_CONFIG: HarnessConfig;
|
|
8
|
+
export declare function createMockWs(): WsHandle & {
|
|
9
|
+
sent: string[];
|
|
10
|
+
closed: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve once `predicate` holds over the frames sent so far, re-checking
|
|
13
|
+
* on each send.
|
|
14
|
+
*
|
|
15
|
+
* Several of the router's paths write to the socket from a fire-and-forget
|
|
16
|
+
* continuation -- the mail redelivery retry is one -- so a test that
|
|
17
|
+
* triggers one has no return value to await. The send is the event.
|
|
18
|
+
*/
|
|
19
|
+
awaitSent(predicate: (sent: readonly string[]) => boolean): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export declare function createAllocatedRouter(config?: Partial<SidecarRouterConfig>): import("./sidecar-handler.js").SidecarRouter & import("./sidecar-handler.js").SidecarAllocationRouter;
|
|
22
|
+
export declare function connectAllocated(router: ReturnType<typeof createSidecarRouter>, agentAddresses?: string[], frameType?: "register" | "reconnect"): Promise<WsHandle & {
|
|
23
|
+
sent: string[];
|
|
24
|
+
closed: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve once `predicate` holds over the frames sent so far, re-checking
|
|
27
|
+
* on each send.
|
|
28
|
+
*
|
|
29
|
+
* Several of the router's paths write to the socket from a fire-and-forget
|
|
30
|
+
* continuation -- the mail redelivery retry is one -- so a test that
|
|
31
|
+
* triggers one has no return value to await. The send is the event.
|
|
32
|
+
*/
|
|
33
|
+
awaitSent(predicate: (sent: readonly string[]) => boolean): Promise<void>;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function parsedFrames(ws: {
|
|
36
|
+
sent: string[];
|
|
37
|
+
}): unknown[];
|
|
38
|
+
export declare function tick(): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createSidecarRouter, } from "./sidecar-handler.js";
|
|
2
|
+
export const TEST_IDENTITY = {
|
|
3
|
+
kind: "allocated",
|
|
4
|
+
sidecarId: "sc-allocated",
|
|
5
|
+
allocationId: "alloc-1",
|
|
6
|
+
tenantId: "tenant-1",
|
|
7
|
+
anchorRunId: "run_anchor",
|
|
8
|
+
workflowRunAddress: "run_anchor@tenant.example",
|
|
9
|
+
generation: 1,
|
|
10
|
+
};
|
|
11
|
+
export const TEST_TARGET = {
|
|
12
|
+
allocationId: TEST_IDENTITY.allocationId,
|
|
13
|
+
generation: TEST_IDENTITY.generation,
|
|
14
|
+
};
|
|
15
|
+
export const TEST_CONFIG = {
|
|
16
|
+
sessionId: "ses-router-test",
|
|
17
|
+
agentId: "workflow",
|
|
18
|
+
tenantId: TEST_IDENTITY.tenantId,
|
|
19
|
+
principalId: "principal-1",
|
|
20
|
+
agentAddress: TEST_IDENTITY.workflowRunAddress,
|
|
21
|
+
systemPrompt: "test",
|
|
22
|
+
tools: [],
|
|
23
|
+
grants: [],
|
|
24
|
+
sources: [
|
|
25
|
+
{
|
|
26
|
+
id: "test-source",
|
|
27
|
+
provider: "test",
|
|
28
|
+
baseURL: "https://api.example.test",
|
|
29
|
+
credentialId: "test-credential",
|
|
30
|
+
model: "test",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
defaultSource: "test-source",
|
|
34
|
+
};
|
|
35
|
+
export function createMockWs() {
|
|
36
|
+
let waiters = [];
|
|
37
|
+
return {
|
|
38
|
+
sent: [],
|
|
39
|
+
closed: false,
|
|
40
|
+
send(data) {
|
|
41
|
+
this.sent.push(data);
|
|
42
|
+
const waking = waiters;
|
|
43
|
+
waiters = [];
|
|
44
|
+
for (const wake of waking)
|
|
45
|
+
wake();
|
|
46
|
+
},
|
|
47
|
+
close() {
|
|
48
|
+
this.closed = true;
|
|
49
|
+
},
|
|
50
|
+
async awaitSent(predicate) {
|
|
51
|
+
for (;;) {
|
|
52
|
+
// Re-checked on every pass, so a frame sent before this call resolves
|
|
53
|
+
// it rather than leaving it waiting for another send.
|
|
54
|
+
const sent = new Promise((resolve) => {
|
|
55
|
+
waiters.push(resolve);
|
|
56
|
+
});
|
|
57
|
+
if (predicate(this.sent))
|
|
58
|
+
return;
|
|
59
|
+
await sent;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function createAllocatedRouter(config = {}) {
|
|
65
|
+
const router = createSidecarRouter({
|
|
66
|
+
authenticateSidecar: async () => TEST_IDENTITY,
|
|
67
|
+
validateSidecarIdentity: async () => true,
|
|
68
|
+
hubPublicKey: "a".repeat(64),
|
|
69
|
+
requestTimeoutMs: 500,
|
|
70
|
+
...config,
|
|
71
|
+
});
|
|
72
|
+
router.fenceAllocation(TEST_TARGET.allocationId, TEST_TARGET.generation);
|
|
73
|
+
return router;
|
|
74
|
+
}
|
|
75
|
+
export async function connectAllocated(router, agentAddresses = [], frameType = "register") {
|
|
76
|
+
const ws = createMockWs();
|
|
77
|
+
router.handleOpen(ws);
|
|
78
|
+
router.handleMessage(ws, JSON.stringify({
|
|
79
|
+
type: frameType,
|
|
80
|
+
sidecarId: TEST_IDENTITY.sidecarId,
|
|
81
|
+
token: "token",
|
|
82
|
+
agentAddresses,
|
|
83
|
+
}));
|
|
84
|
+
await tick();
|
|
85
|
+
return ws;
|
|
86
|
+
}
|
|
87
|
+
export function parsedFrames(ws) {
|
|
88
|
+
return ws.sent.map((raw) => {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
return parsed;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function tick() {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
95
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DB } from "@intx/db";
|
|
2
|
+
import type { SidecarCredentialResolver } from "../sidecar-allocation/contracts.js";
|
|
2
3
|
import type { SidecarAuthenticator } from "./sidecar-handler.js";
|
|
3
4
|
export type CreateSidecarTokenAuthenticatorDeps = {
|
|
4
5
|
db: DB["db"];
|
|
@@ -12,4 +13,5 @@ export type CreateSidecarTokenAuthenticatorDeps = {
|
|
|
12
13
|
* `sidecarId` on the frame is ignored: identity is derived from the
|
|
13
14
|
* token alone.
|
|
14
15
|
*/
|
|
15
|
-
export declare function
|
|
16
|
+
export declare function createSidecarCredentialResolver({ db, }: CreateSidecarTokenAuthenticatorDeps): SidecarCredentialResolver;
|
|
17
|
+
export declare function createSidecarTokenAuthenticator(deps: CreateSidecarTokenAuthenticatorDeps): SidecarAuthenticator;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
1
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
2
2
|
import { sha256 } from "@intx/crypto";
|
|
3
|
-
import { sidecar } from "@intx/db/schema";
|
|
3
|
+
import { sidecar, sidecarAllocation, workflowProbe, workflowRun, } from "@intx/db/schema";
|
|
4
4
|
/**
|
|
5
5
|
* Builds an authenticator that verifies a sidecar's presented token
|
|
6
6
|
* against the per-sidecar hash stored on the `sidecar` table. The token
|
|
@@ -10,15 +10,86 @@ import { sidecar } from "@intx/db/schema";
|
|
|
10
10
|
* `sidecarId` on the frame is ignored: identity is derived from the
|
|
11
11
|
* token alone.
|
|
12
12
|
*/
|
|
13
|
-
export function
|
|
14
|
-
|
|
13
|
+
export function createSidecarCredentialResolver({ db, }) {
|
|
14
|
+
async function resolve(token) {
|
|
15
15
|
const tokenHash = await sha256(token);
|
|
16
16
|
const row = await db.query.sidecar.findFirst({
|
|
17
17
|
where: eq(sidecar.tokenHashSha256, tokenHash),
|
|
18
18
|
});
|
|
19
|
-
if (row === undefined)
|
|
19
|
+
if (row === undefined)
|
|
20
20
|
return null;
|
|
21
|
+
const allocation = await db.query.sidecarAllocation.findFirst({
|
|
22
|
+
where: eq(sidecarAllocation.sidecarId, row.id),
|
|
23
|
+
});
|
|
24
|
+
if (allocation !== undefined) {
|
|
25
|
+
if (allocation.status !== "provisioning" &&
|
|
26
|
+
allocation.status !== "allocated") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const anchor = await db.query.workflowRun.findFirst({
|
|
30
|
+
columns: { address: true },
|
|
31
|
+
where: eq(workflowRun.id, allocation.anchorRunId),
|
|
32
|
+
});
|
|
33
|
+
if (anchor?.address === null || anchor?.address === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
kind: "allocated",
|
|
37
|
+
sidecarId: row.id,
|
|
38
|
+
allocationId: allocation.id,
|
|
39
|
+
tenantId: allocation.tenantId,
|
|
40
|
+
anchorRunId: allocation.anchorRunId,
|
|
41
|
+
workflowRunAddress: anchor.address,
|
|
42
|
+
generation: allocation.generation,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const probe = await db.query.workflowProbe.findFirst({
|
|
46
|
+
where: eq(workflowProbe.sidecarId, row.id),
|
|
47
|
+
});
|
|
48
|
+
if (probe === undefined ||
|
|
49
|
+
(probe.status !== "provisioning" && probe.status !== "probing")) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "probe",
|
|
54
|
+
sidecarId: row.id,
|
|
55
|
+
allocationId: probe.id,
|
|
56
|
+
tenantId: probe.tenantId,
|
|
57
|
+
generation: probe.generation,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function isCurrent(identity, use) {
|
|
61
|
+
if (identity.kind === "probe") {
|
|
62
|
+
const statuses = use === "registration"
|
|
63
|
+
? ["provisioning", "probing"]
|
|
64
|
+
: ["probing"];
|
|
65
|
+
const probe = await db.query.workflowProbe.findFirst({
|
|
66
|
+
columns: { id: true },
|
|
67
|
+
where: and(eq(workflowProbe.id, identity.allocationId), eq(workflowProbe.sidecarId, identity.sidecarId), eq(workflowProbe.tenantId, identity.tenantId), eq(workflowProbe.generation, identity.generation), inArray(workflowProbe.status, statuses)),
|
|
68
|
+
});
|
|
69
|
+
return probe !== undefined;
|
|
21
70
|
}
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
const statuses = use === "registration"
|
|
72
|
+
? ["provisioning", "allocated"]
|
|
73
|
+
: ["allocated"];
|
|
74
|
+
const allocation = await db.query.sidecarAllocation.findFirst({
|
|
75
|
+
where: and(eq(sidecarAllocation.id, identity.allocationId), eq(sidecarAllocation.sidecarId, identity.sidecarId), eq(sidecarAllocation.tenantId, identity.tenantId), eq(sidecarAllocation.anchorRunId, identity.anchorRunId), eq(sidecarAllocation.generation, identity.generation), inArray(sidecarAllocation.status, statuses)),
|
|
76
|
+
});
|
|
77
|
+
if (allocation === undefined)
|
|
78
|
+
return false;
|
|
79
|
+
if (use === "registration")
|
|
80
|
+
return true;
|
|
81
|
+
if (allocation.ensureAcceptedGeneration !== identity.generation) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const anchor = await db.query.workflowRun.findFirst({
|
|
85
|
+
columns: { address: true },
|
|
86
|
+
where: eq(workflowRun.id, identity.anchorRunId),
|
|
87
|
+
});
|
|
88
|
+
return anchor?.address === identity.workflowRunAddress;
|
|
89
|
+
}
|
|
90
|
+
return { resolve, isCurrent };
|
|
91
|
+
}
|
|
92
|
+
export function createSidecarTokenAuthenticator(deps) {
|
|
93
|
+
const resolver = createSidecarCredentialResolver(deps);
|
|
94
|
+
return async ({ token }) => resolver.resolve(token);
|
|
24
95
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intx/hub-sessions",
|
|
3
|
-
"
|
|
3
|
+
"description": "Session-orchestration substrate for the hub: sidecar router, session, and asset services",
|
|
4
|
+
"version": "0.4.0",
|
|
4
5
|
"license": "LGPL-2.1-only",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"exports": {
|
|
@@ -16,18 +17,18 @@
|
|
|
16
17
|
}
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
19
|
-
"@intx/agent": "0.
|
|
20
|
-
"@intx/crypto": "0.
|
|
21
|
-
"@intx/db": "0.
|
|
22
|
-
"@intx/hub-common": "0.
|
|
23
|
-
"@intx/log": "0.
|
|
24
|
-
"@intx/mime": "0.
|
|
25
|
-
"@intx/pack-transport": "0.
|
|
26
|
-
"@intx/storage-isogit": "0.
|
|
27
|
-
"@intx/tool-packaging": "0.
|
|
28
|
-
"@intx/types": "0.
|
|
29
|
-
"@intx/workflow": "0.
|
|
30
|
-
"@intx/workflow-deploy": "0.
|
|
20
|
+
"@intx/agent": "0.4.0",
|
|
21
|
+
"@intx/crypto": "0.4.0",
|
|
22
|
+
"@intx/db": "0.4.0",
|
|
23
|
+
"@intx/hub-common": "0.4.0",
|
|
24
|
+
"@intx/log": "0.4.0",
|
|
25
|
+
"@intx/mime": "0.4.0",
|
|
26
|
+
"@intx/pack-transport": "0.4.0",
|
|
27
|
+
"@intx/storage-isogit": "0.4.0",
|
|
28
|
+
"@intx/tool-packaging": "0.4.0",
|
|
29
|
+
"@intx/types": "0.4.0",
|
|
30
|
+
"@intx/workflow": "0.4.0",
|
|
31
|
+
"@intx/workflow-deploy": "0.4.0",
|
|
31
32
|
"arktype": "^2.1.29",
|
|
32
33
|
"drizzle-orm": "^0.45.1",
|
|
33
34
|
"isomorphic-git": "^1.27.2",
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type AvailableSkillEntry = {
|
|
2
|
-
/** Qualified skill identifier in the form `<asset.name>/<skill-name>`. */
|
|
3
|
-
qualifiedName: string;
|
|
4
|
-
/** SKILL.md frontmatter description, verbatim. */
|
|
5
|
-
description: string;
|
|
6
|
-
/** Workspace-relative path the agent's `read_file` should target,
|
|
7
|
-
* shaped like `workspace/<mountPath>/<skill-name>/`. */
|
|
8
|
-
workspacePath: string;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Render the `<available_skills>` stanza appended to the agent's
|
|
12
|
-
* system prompt. Returns the empty string when `entries` is empty —
|
|
13
|
-
* an empty `<available_skills></available_skills>` wrapper would be
|
|
14
|
-
* misleading noise for agents with no skills attached.
|
|
15
|
-
*
|
|
16
|
-
* Values are XML-escaped at the boundary. The skill kind handler
|
|
17
|
-
* already rejects descriptions containing literal `<` or `>` so the
|
|
18
|
-
* `&` escape is the practical case in production; the others are
|
|
19
|
-
* defensive.
|
|
20
|
-
*/
|
|
21
|
-
export declare function buildAvailableSkillsStanza(entries: AvailableSkillEntry[]): string;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render the `<available_skills>` stanza appended to the agent's
|
|
3
|
-
* system prompt. Returns the empty string when `entries` is empty —
|
|
4
|
-
* an empty `<available_skills></available_skills>` wrapper would be
|
|
5
|
-
* misleading noise for agents with no skills attached.
|
|
6
|
-
*
|
|
7
|
-
* Values are XML-escaped at the boundary. The skill kind handler
|
|
8
|
-
* already rejects descriptions containing literal `<` or `>` so the
|
|
9
|
-
* `&` escape is the practical case in production; the others are
|
|
10
|
-
* defensive.
|
|
11
|
-
*/
|
|
12
|
-
export function buildAvailableSkillsStanza(entries) {
|
|
13
|
-
if (entries.length === 0) {
|
|
14
|
-
return "";
|
|
15
|
-
}
|
|
16
|
-
const lines = ["<available_skills>"];
|
|
17
|
-
for (const entry of entries) {
|
|
18
|
-
lines.push(" <skill>");
|
|
19
|
-
lines.push(` <name>${escapeXml(entry.qualifiedName)}</name>`);
|
|
20
|
-
lines.push(` <description>${escapeXml(entry.description)}</description>`);
|
|
21
|
-
lines.push(` <path>${escapeXml(entry.workspacePath)}</path>`);
|
|
22
|
-
lines.push(" </skill>");
|
|
23
|
-
}
|
|
24
|
-
lines.push("</available_skills>");
|
|
25
|
-
return lines.join("\n");
|
|
26
|
-
}
|
|
27
|
-
function escapeXml(value) {
|
|
28
|
-
return value
|
|
29
|
-
.replaceAll("&", "&")
|
|
30
|
-
.replaceAll("<", "<")
|
|
31
|
-
.replaceAll(">", ">");
|
|
32
|
-
}
|