@ctliz/agent-intercom-codex 0.11.0-connect.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/.codex-plugin/plugin.json +39 -0
- package/.mcp.json +10 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +572 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/assets/logo-generated.png +0 -0
- package/assets/logo.svg +22 -0
- package/boss-control-outbox.ts +195 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +464 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +54 -0
- package/broker/boss-adapter.ts +531 -0
- package/broker/boss-control-ledger.ts +306 -0
- package/broker/broker.ts +2327 -0
- package/broker/client.ts +1111 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +453 -0
- package/codex/app-server-client.ts +562 -0
- package/codex/boss-client.ts +34 -0
- package/codex/bridge-config.ts +282 -0
- package/codex/bridge-daemon.ts +913 -0
- package/codex/clipboard.ts +142 -0
- package/codex/coi.ts +802 -0
- package/codex/contact.ts +54 -0
- package/codex/mcp-protocol.ts +248 -0
- package/codex/runtime.ts +488 -0
- package/codex/server.ts +60 -0
- package/codex/team.ts +249 -0
- package/codex/tui-input.ts +174 -0
- package/config.ts +195 -0
- package/dist/bridge-daemon.mjs +3781 -0
- package/dist/broker.mjs +3012 -0
- package/dist/build-info.json +12 -0
- package/dist/codex-server.mjs +2890 -0
- package/dist/coi.mjs +4686 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +89 -0
- package/protocol-v4/contract.ts +27 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build-info.mjs +58 -0
- package/scripts/build.mjs +55 -0
- package/skills/codex-intercom/SKILL.md +104 -0
- package/tsconfig.json +16 -0
- package/types.ts +225 -0
package/durable-json.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { closeSync, fsyncSync, openSync, renameSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { INTERCOM_RUNTIME_FILE_MODE, restrictIntercomRuntimeFile } from "./broker/paths.ts";
|
|
5
|
+
|
|
6
|
+
export function writeDurableJson(filePath: string, value: unknown): void {
|
|
7
|
+
const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
8
|
+
writeFileSync(temporaryPath, JSON.stringify(value), { encoding: "utf-8", mode: INTERCOM_RUNTIME_FILE_MODE });
|
|
9
|
+
const fileDescriptor = openSync(temporaryPath, "r");
|
|
10
|
+
try {
|
|
11
|
+
fsyncSync(fileDescriptor);
|
|
12
|
+
} finally {
|
|
13
|
+
closeSync(fileDescriptor);
|
|
14
|
+
}
|
|
15
|
+
renameSync(temporaryPath, filePath);
|
|
16
|
+
restrictIntercomRuntimeFile(filePath);
|
|
17
|
+
if (process.platform !== "win32") {
|
|
18
|
+
const directoryDescriptor = openSync(dirname(filePath), "r");
|
|
19
|
+
try {
|
|
20
|
+
fsyncSync(directoryDescriptor);
|
|
21
|
+
} finally {
|
|
22
|
+
closeSync(directoryDescriptor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nico Bailon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { ensureIntercomRuntimeDir, getIntercomDirPath, INTERCOM_DIR_MODE, restrictIntercomRuntimeFile } from "./broker/paths.ts";
|
|
5
|
+
import { writeDurableJson } from "./durable-json.ts";
|
|
6
|
+
import type { Message } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const OUTBOX_STATE_VERSION = 1;
|
|
9
|
+
const MAX_OUTBOX_MESSAGES = 256;
|
|
10
|
+
|
|
11
|
+
export interface StoredOutboundMessage {
|
|
12
|
+
to: string;
|
|
13
|
+
message: Message;
|
|
14
|
+
queuedAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface OutboxState {
|
|
18
|
+
version: typeof OUTBOX_STATE_VERSION;
|
|
19
|
+
entries: StoredOutboundMessage[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fingerprint(entry: Pick<StoredOutboundMessage, "to" | "message">): string {
|
|
23
|
+
return JSON.stringify({
|
|
24
|
+
to: entry.to,
|
|
25
|
+
replyTo: entry.message.replyTo,
|
|
26
|
+
expectsReply: entry.message.expectsReply,
|
|
27
|
+
content: entry.message.content,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isStoredOutboundMessage(value: unknown): value is StoredOutboundMessage {
|
|
32
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
33
|
+
const entry = value as Record<string, unknown>;
|
|
34
|
+
if (typeof entry.to !== "string" || typeof entry.queuedAt !== "number") return false;
|
|
35
|
+
if (typeof entry.message !== "object" || entry.message === null || Array.isArray(entry.message)) return false;
|
|
36
|
+
const message = entry.message as Record<string, unknown>;
|
|
37
|
+
return typeof message.id === "string"
|
|
38
|
+
&& typeof message.timestamp === "number"
|
|
39
|
+
&& typeof message.content === "object"
|
|
40
|
+
&& message.content !== null
|
|
41
|
+
&& typeof (message.content as Record<string, unknown>).text === "string";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fileName(sessionId: string): string {
|
|
45
|
+
return `${createHash("sha256").update(sessionId).digest("hex")}.json`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class PersistentOutboundOutbox {
|
|
49
|
+
private readonly directory: string;
|
|
50
|
+
private readonly filePath: string;
|
|
51
|
+
private state: OutboxState;
|
|
52
|
+
|
|
53
|
+
constructor(sessionId: string, intercomDir: string = getIntercomDirPath()) {
|
|
54
|
+
ensureIntercomRuntimeDir(intercomDir);
|
|
55
|
+
this.directory = join(intercomDir, "outbox");
|
|
56
|
+
mkdirSync(this.directory, { recursive: true, mode: INTERCOM_DIR_MODE });
|
|
57
|
+
if (process.platform !== "win32") chmodSync(this.directory, INTERCOM_DIR_MODE);
|
|
58
|
+
this.filePath = join(this.directory, fileName(sessionId));
|
|
59
|
+
this.state = this.load();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
list(): StoredOutboundMessage[] {
|
|
63
|
+
return this.state.entries.map((entry) => ({ ...entry, message: { ...entry.message, content: { ...entry.message.content } } }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
enqueue(to: string, message: Message): "added" | "existing" {
|
|
67
|
+
const existing = this.state.entries.find((entry) => entry.message.id === message.id);
|
|
68
|
+
if (existing) {
|
|
69
|
+
if (fingerprint(existing) !== fingerprint({ to, message })) {
|
|
70
|
+
throw new Error(`Message ID ${message.id} is already queued with a different payload`);
|
|
71
|
+
}
|
|
72
|
+
return "existing";
|
|
73
|
+
}
|
|
74
|
+
if (this.state.entries.length >= MAX_OUTBOX_MESSAGES) {
|
|
75
|
+
throw new Error(`Durable outbox is full (${MAX_OUTBOX_MESSAGES} messages)`);
|
|
76
|
+
}
|
|
77
|
+
this.state.entries.push({ to, message, queuedAt: Date.now() });
|
|
78
|
+
this.persist();
|
|
79
|
+
return "added";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
remove(messageId: string): void {
|
|
83
|
+
const remaining = this.state.entries.filter((entry) => entry.message.id !== messageId);
|
|
84
|
+
if (remaining.length === this.state.entries.length) return;
|
|
85
|
+
this.state.entries = remaining;
|
|
86
|
+
this.persist();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
clear(): void {
|
|
90
|
+
if (this.state.entries.length === 0) return;
|
|
91
|
+
this.state.entries = [];
|
|
92
|
+
this.persist();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private load(): OutboxState {
|
|
96
|
+
if (!existsSync(this.filePath)) return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
97
|
+
try {
|
|
98
|
+
const parsed: unknown = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
99
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("expected object");
|
|
100
|
+
const state = parsed as Record<string, unknown>;
|
|
101
|
+
if (state.version !== OUTBOX_STATE_VERSION || !Array.isArray(state.entries) || !state.entries.every(isStoredOutboundMessage)) {
|
|
102
|
+
throw new Error("invalid outbox state");
|
|
103
|
+
}
|
|
104
|
+
return { version: OUTBOX_STATE_VERSION, entries: state.entries };
|
|
105
|
+
} catch {
|
|
106
|
+
const corruptPath = `${this.filePath}.corrupt-${Date.now()}`;
|
|
107
|
+
renameSync(this.filePath, corruptPath);
|
|
108
|
+
restrictIntercomRuntimeFile(corruptPath);
|
|
109
|
+
return { version: OUTBOX_STATE_VERSION, entries: [] };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private persist(): void {
|
|
114
|
+
writeDurableJson(this.filePath, this.state);
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ctliz/agent-intercom-codex",
|
|
3
|
+
"version": "0.11.0-connect.2",
|
|
4
|
+
"description": "Codex MCP plugin for direct local messaging with Pi and Codex coding-agent sessions.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.19.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/codex-server.mjs",
|
|
11
|
+
"bin": {
|
|
12
|
+
"codex-intercom-mcp": "dist/codex-server.mjs",
|
|
13
|
+
"codex-intercom-bridge": "dist/bridge-daemon.mjs",
|
|
14
|
+
"coi": "dist/coi.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"assets/**/*",
|
|
18
|
+
"dist/**/*",
|
|
19
|
+
"codex/**/*.ts",
|
|
20
|
+
"!codex/**/*.test.ts",
|
|
21
|
+
"broker/**/*.ts",
|
|
22
|
+
"!broker/**/*.test.ts",
|
|
23
|
+
"protocol-v4/**/*.ts",
|
|
24
|
+
"!protocol-v4/**/*.test.ts",
|
|
25
|
+
"provider/protected-service.ts",
|
|
26
|
+
"provider/provider.mjs",
|
|
27
|
+
"!provider/entry.ts",
|
|
28
|
+
"scripts/**/*",
|
|
29
|
+
"!scripts/**/*.test.mjs",
|
|
30
|
+
"!scripts/build-protected-provider.mjs",
|
|
31
|
+
"skills/**/*",
|
|
32
|
+
"types.ts",
|
|
33
|
+
"config.ts",
|
|
34
|
+
"tsconfig.json",
|
|
35
|
+
"durable-json.ts",
|
|
36
|
+
"boss-control-outbox.ts",
|
|
37
|
+
"outbound-outbox.ts",
|
|
38
|
+
".codex-plugin/**/*",
|
|
39
|
+
".mcp.json",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"THIRD_PARTY_NOTICES.md",
|
|
43
|
+
"licenses/**/*",
|
|
44
|
+
"COPYRIGHT",
|
|
45
|
+
"LICENSE_TRANSITION.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "node scripts/build.mjs",
|
|
49
|
+
"build:protected-provider": "node scripts/build-protected-provider.mjs",
|
|
50
|
+
"prepack": "npm run build:protected-provider",
|
|
51
|
+
"prepare": "npm run build",
|
|
52
|
+
"codex:mcp": "tsx codex/server.ts",
|
|
53
|
+
"codex:bridge": "tsx codex/bridge-daemon.ts",
|
|
54
|
+
"coi": "tsx codex/coi.ts",
|
|
55
|
+
"test": "node --import tsx --test broker/*.test.ts boss-control-outbox.test.ts outbound-outbox.test.ts codex/*.test.ts protocol-v4/*.test.ts provider/*.test.ts scripts/*.test.mjs",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"codex",
|
|
60
|
+
"mcp",
|
|
61
|
+
"intercom",
|
|
62
|
+
"multi-agent",
|
|
63
|
+
"pi"
|
|
64
|
+
],
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@ctliz/agent-intercom-core": "0.1.0"
|
|
67
|
+
},
|
|
68
|
+
"optionalDependencies": {
|
|
69
|
+
"node-pty": "^1.1.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@ctliz/agent-intercom-core": "git+https://github.com/ctliz/agent-intercom-core.git#37e074970e2a9de32a16fc325607c3b476b0bd45",
|
|
73
|
+
"@types/node": "^24.0.0",
|
|
74
|
+
"esbuild": "^0.28.1",
|
|
75
|
+
"tsx": "^4.20.0",
|
|
76
|
+
"typescript": "^6.0.3"
|
|
77
|
+
},
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"access": "public"
|
|
80
|
+
},
|
|
81
|
+
"repository": {
|
|
82
|
+
"type": "git",
|
|
83
|
+
"url": "git+https://github.com/ctliz/agent-intercom-codex.git"
|
|
84
|
+
},
|
|
85
|
+
"homepage": "https://github.com/ctliz/agent-intercom-codex#readme",
|
|
86
|
+
"bugs": {
|
|
87
|
+
"url": "https://github.com/ctliz/agent-intercom-codex/issues"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTERCOM_PROTOCOL_NAME,
|
|
3
|
+
INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
|
|
4
|
+
INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
|
|
5
|
+
INTERCOM_PROTOCOL_V4_VECTORS,
|
|
6
|
+
INTERCOM_PROTOCOL_VERSION,
|
|
7
|
+
INTERCOM_SCOPE_ENV,
|
|
8
|
+
INTERCOM_SCOPE_ID_PATTERN,
|
|
9
|
+
INTERCOM_SCOPE_ID_PATTERN_SOURCE,
|
|
10
|
+
intercomScopeIdFromEnv,
|
|
11
|
+
parseIntercomScopeId,
|
|
12
|
+
sameIntercomScope,
|
|
13
|
+
} from "@ctliz/agent-intercom-core/protocol-v4";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
INTERCOM_PROTOCOL_NAME,
|
|
17
|
+
INTERCOM_PROTOCOL_V4_SEMANTICS_HASH,
|
|
18
|
+
INTERCOM_PROTOCOL_V4_VECTOR_SCHEMA_VERSION,
|
|
19
|
+
INTERCOM_PROTOCOL_V4_VECTORS,
|
|
20
|
+
INTERCOM_PROTOCOL_VERSION,
|
|
21
|
+
INTERCOM_SCOPE_ENV,
|
|
22
|
+
INTERCOM_SCOPE_ID_PATTERN,
|
|
23
|
+
INTERCOM_SCOPE_ID_PATTERN_SOURCE,
|
|
24
|
+
intercomScopeIdFromEnv,
|
|
25
|
+
parseIntercomScopeId,
|
|
26
|
+
sameIntercomScope,
|
|
27
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { types as nodeUtilTypes } from "node:util";
|
|
2
|
+
import { BROKER_PROTECTED_PROVIDER_ROOT } from "@ctliz/agent-intercom-core/boss";
|
|
3
|
+
|
|
4
|
+
export const CODEX_BOSS_PROTECTED_PROVIDER_ID = "codex" as const;
|
|
5
|
+
export const CODEX_BOSS_PROTECTED_PROVIDER_PACKAGE = "@ctliz/agent-intercom-codex" as const;
|
|
6
|
+
export const CODEX_BOSS_PROTECTED_PROVIDER_ARTIFACT_PATH =
|
|
7
|
+
`${BROKER_PROTECTED_PROVIDER_ROOT}${CODEX_BOSS_PROTECTED_PROVIDER_ID}/provider.mjs` as const;
|
|
8
|
+
export const BOSS_PROTECTED_SERVICE_UNAVAILABLE = "BOSS_PROTECTED_SERVICE_UNAVAILABLE" as const;
|
|
9
|
+
|
|
10
|
+
const CODEX_BOSS_PROTECTED_PROVIDER_MODE = "0555" as const;
|
|
11
|
+
const CANONICAL_SEMANTIC_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
12
|
+
const CANDIDATE_KEYS = [
|
|
13
|
+
"adapterId",
|
|
14
|
+
"providerPackage",
|
|
15
|
+
"providerVersion",
|
|
16
|
+
"providerDigest",
|
|
17
|
+
"artifactPath",
|
|
18
|
+
"artifactOwnerUid",
|
|
19
|
+
"artifactOwnerGid",
|
|
20
|
+
"artifactMode",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export interface CodexBossProtectedProviderArtifactCandidate {
|
|
24
|
+
adapterId: typeof CODEX_BOSS_PROTECTED_PROVIDER_ID;
|
|
25
|
+
providerPackage: typeof CODEX_BOSS_PROTECTED_PROVIDER_PACKAGE;
|
|
26
|
+
providerVersion: string;
|
|
27
|
+
providerDigest: string;
|
|
28
|
+
artifactPath: typeof CODEX_BOSS_PROTECTED_PROVIDER_ARTIFACT_PATH;
|
|
29
|
+
artifactOwnerUid: 0;
|
|
30
|
+
artifactOwnerGid: 0;
|
|
31
|
+
artifactMode: typeof CODEX_BOSS_PROTECTED_PROVIDER_MODE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type CodexBossProtectedServiceErrorCode =
|
|
35
|
+
| "INVALID_CODEX_PROTECTED_PROVIDER_CANDIDATE"
|
|
36
|
+
| typeof BOSS_PROTECTED_SERVICE_UNAVAILABLE;
|
|
37
|
+
|
|
38
|
+
export class CodexBossProtectedServiceError extends Error {
|
|
39
|
+
readonly code: CodexBossProtectedServiceErrorCode;
|
|
40
|
+
readonly path: string;
|
|
41
|
+
|
|
42
|
+
constructor(code: CodexBossProtectedServiceErrorCode, path: string, message: string) {
|
|
43
|
+
super(`${path}: ${message}`);
|
|
44
|
+
this.name = "CodexBossProtectedServiceError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.path = path;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function invalid(path: string, message: string): never {
|
|
51
|
+
throw new CodexBossProtectedServiceError(
|
|
52
|
+
"INVALID_CODEX_PROTECTED_PROVIDER_CANDIDATE",
|
|
53
|
+
path,
|
|
54
|
+
message,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertExactOwnDataCandidate(value: unknown): asserts value is Record<string, unknown> {
|
|
59
|
+
const path = "$candidate";
|
|
60
|
+
if (
|
|
61
|
+
typeof value !== "object"
|
|
62
|
+
|| value === null
|
|
63
|
+
|| nodeUtilTypes.isProxy(value)
|
|
64
|
+
|| Array.isArray(value)
|
|
65
|
+
|| Object.getPrototypeOf(value) !== Object.prototype
|
|
66
|
+
) {
|
|
67
|
+
invalid(path, "must be a non-proxy plain data object");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const keys = Reflect.ownKeys(value);
|
|
71
|
+
if (
|
|
72
|
+
keys.length !== CANDIDATE_KEYS.length
|
|
73
|
+
|| keys.some((key) => typeof key !== "string" || !CANDIDATE_KEYS.includes(key as typeof CANDIDATE_KEYS[number]))
|
|
74
|
+
) {
|
|
75
|
+
invalid(path, "must contain exactly the canonical unsigned Codex provider candidate fields");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const key of CANDIDATE_KEYS) {
|
|
79
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
80
|
+
if (
|
|
81
|
+
descriptor === undefined
|
|
82
|
+
|| !descriptor.enumerable
|
|
83
|
+
|| !Object.hasOwn(descriptor, "value")
|
|
84
|
+
) {
|
|
85
|
+
invalid(`${path}.${key}`, "must be an own enumerable data property");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ownValue(value: Record<string, unknown>, key: typeof CANDIDATE_KEYS[number]): unknown {
|
|
91
|
+
return Object.getOwnPropertyDescriptor(value, key)!.value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readProviderVersion(value: unknown): string {
|
|
95
|
+
if (
|
|
96
|
+
typeof value !== "string"
|
|
97
|
+
|| value.length > 128
|
|
98
|
+
|| /[\r\n\u2028\u2029]/.test(value)
|
|
99
|
+
|| !CANONICAL_SEMANTIC_VERSION.test(value)
|
|
100
|
+
) {
|
|
101
|
+
invalid("$candidate.providerVersion", "must be a canonical semantic version");
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readProviderDigest(value: unknown): string {
|
|
107
|
+
if (typeof value !== "string" || value.length !== 64 || !/^[a-f0-9]{64}$/.test(value)) {
|
|
108
|
+
invalid("$candidate.providerDigest", "must be a lowercase SHA-256 digest");
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Normalize an unsigned release candidate for the packaged Codex provider.
|
|
115
|
+
* This parser cannot verify installation, signatures, service identities, or
|
|
116
|
+
* authority. Its frozen result remains explicitly non-authoritative.
|
|
117
|
+
*/
|
|
118
|
+
export function parseCodexBossProtectedProviderArtifactCandidate(
|
|
119
|
+
value: unknown,
|
|
120
|
+
): Readonly<CodexBossProtectedProviderArtifactCandidate> {
|
|
121
|
+
assertExactOwnDataCandidate(value);
|
|
122
|
+
|
|
123
|
+
if (ownValue(value, "adapterId") !== CODEX_BOSS_PROTECTED_PROVIDER_ID) {
|
|
124
|
+
invalid("$candidate.adapterId", "must identify the Codex provider");
|
|
125
|
+
}
|
|
126
|
+
if (ownValue(value, "providerPackage") !== CODEX_BOSS_PROTECTED_PROVIDER_PACKAGE) {
|
|
127
|
+
invalid("$candidate.providerPackage", "must identify the canonical Codex package");
|
|
128
|
+
}
|
|
129
|
+
if (ownValue(value, "artifactPath") !== CODEX_BOSS_PROTECTED_PROVIDER_ARTIFACT_PATH) {
|
|
130
|
+
invalid("$candidate.artifactPath", "must equal the canonical protected Codex provider path");
|
|
131
|
+
}
|
|
132
|
+
if (ownValue(value, "artifactOwnerUid") !== 0 || ownValue(value, "artifactOwnerGid") !== 0) {
|
|
133
|
+
invalid("$candidate.artifactOwnerUid", "must describe a root:root artifact");
|
|
134
|
+
}
|
|
135
|
+
if (ownValue(value, "artifactMode") !== CODEX_BOSS_PROTECTED_PROVIDER_MODE) {
|
|
136
|
+
invalid("$candidate.artifactMode", "must be read/execute-only mode 0555");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
adapterId: CODEX_BOSS_PROTECTED_PROVIDER_ID,
|
|
141
|
+
providerPackage: CODEX_BOSS_PROTECTED_PROVIDER_PACKAGE,
|
|
142
|
+
providerVersion: readProviderVersion(ownValue(value, "providerVersion")),
|
|
143
|
+
providerDigest: readProviderDigest(ownValue(value, "providerDigest")),
|
|
144
|
+
artifactPath: CODEX_BOSS_PROTECTED_PROVIDER_ARTIFACT_PATH,
|
|
145
|
+
artifactOwnerUid: 0,
|
|
146
|
+
artifactOwnerGid: 0,
|
|
147
|
+
artifactMode: CODEX_BOSS_PROTECTED_PROVIDER_MODE,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Production ensure is intentionally unavailable until a protected
|
|
153
|
+
* provisioner supplies release and service identity facts outside caller data.
|
|
154
|
+
* The request is never inspected while that provisioner is absent.
|
|
155
|
+
*/
|
|
156
|
+
export function ensureCodexBossProtectedService(_request: unknown): never {
|
|
157
|
+
throw new CodexBossProtectedServiceError(
|
|
158
|
+
BOSS_PROTECTED_SERVICE_UNAVAILABLE,
|
|
159
|
+
"$provisioner",
|
|
160
|
+
"the protected Codex broker service provisioner is not installed",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Generated from provider/entry.ts by scripts/build-protected-provider.mjs.
|
|
2
|
+
// Do not edit this artifact directly.
|
|
3
|
+
const supportedBossFeature = Object.freeze({
|
|
4
|
+
version: "intercom.broker-feature.v1",
|
|
5
|
+
feature: "boss-run-v1",
|
|
6
|
+
featureVersion: 1,
|
|
7
|
+
semanticsHash: "8943eb60d29afa5264322b5cc7df3de245b01b1cf48a8cbf9cfb6188b02fcfa9",
|
|
8
|
+
controlEnvelopeVersion: 1,
|
|
9
|
+
capabilityDigest: "239bee8bb64cc8c149d49ac00c7396f33f375a89a1d6dea6bd86ff840d551a59",
|
|
10
|
+
});
|
|
11
|
+
/** Immutable build-time contract only; it is not an installed-provider claim. */
|
|
12
|
+
export const CODEX_BOSS_PROTECTED_PROVIDER_BUILD_IDENTITY = Object.freeze({
|
|
13
|
+
contractVersion: "codex.boss-protected-provider.v1",
|
|
14
|
+
adapterId: "codex",
|
|
15
|
+
providerPackage: "@ctliz/agent-intercom-codex",
|
|
16
|
+
supportedBaseProtocolVersions: Object.freeze([4]),
|
|
17
|
+
supportedFeatures: Object.freeze([supportedBossFeature]),
|
|
18
|
+
protocolFeatureContractHash: "dae30efe2c48d2de0fe72a7ebdfd107d3feaefc180d42056ba05df6088a94364",
|
|
19
|
+
authoritative: false,
|
|
20
|
+
providerStartAvailable: false,
|
|
21
|
+
bossAdvertisementEnabled: false,
|
|
22
|
+
});
|
|
23
|
+
const BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE = "BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE";
|
|
24
|
+
class CodexBossProtectedProviderStartUnavailableError extends Error {
|
|
25
|
+
code = BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE;
|
|
26
|
+
constructor() {
|
|
27
|
+
super(`${BOSS_PROTECTED_PROVIDER_START_UNAVAILABLE}: protected provider execution is not installed`);
|
|
28
|
+
this.name = "CodexBossProtectedProviderStartUnavailableError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** A later protected service may replace this dormant entry after release gates. */
|
|
32
|
+
export function startCodexBossProtectedProvider(_request) {
|
|
33
|
+
throw new CodexBossProtectedProviderStartUnavailableError();
|
|
34
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const RUNTIME_ROOTS = [
|
|
6
|
+
"broker",
|
|
7
|
+
"codex",
|
|
8
|
+
];
|
|
9
|
+
const RUNTIME_FILES = [
|
|
10
|
+
"config.ts",
|
|
11
|
+
"boss-control-outbox.ts",
|
|
12
|
+
"durable-json.ts",
|
|
13
|
+
"outbound-outbox.ts",
|
|
14
|
+
"package.json",
|
|
15
|
+
"scripts/build-info.mjs",
|
|
16
|
+
"scripts/build.mjs",
|
|
17
|
+
"types.ts",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
async function runtimeFiles(root) {
|
|
21
|
+
const files = [];
|
|
22
|
+
async function walk(path) {
|
|
23
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
24
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
25
|
+
const child = join(path, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
await walk(child);
|
|
28
|
+
} else if (entry.isFile() && !entry.name.endsWith(".test.ts")) {
|
|
29
|
+
files.push(child);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const directory of RUNTIME_ROOTS) await walk(join(root, directory));
|
|
34
|
+
for (const file of RUNTIME_FILES) files.push(join(root, file));
|
|
35
|
+
return files.sort((a, b) => relative(root, a).localeCompare(relative(root, b)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function computeRuntimeSourceSha256(root = process.cwd()) {
|
|
39
|
+
const resolvedRoot = resolve(root);
|
|
40
|
+
const hash = createHash("sha256");
|
|
41
|
+
for (const path of await runtimeFiles(resolvedRoot)) {
|
|
42
|
+
const name = relative(resolvedRoot, path).replaceAll("\\", "/");
|
|
43
|
+
hash.update(name);
|
|
44
|
+
hash.update("\0");
|
|
45
|
+
hash.update(await readFile(path));
|
|
46
|
+
hash.update("\0");
|
|
47
|
+
}
|
|
48
|
+
return hash.digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildIdentityLine({ packageName, version, target, sourceSha256 }) {
|
|
52
|
+
return `[agent-intercom-build] package=${packageName} version=${version} target=${target} sourceSha256=${sourceSha256}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildBanner(identity, shebang = "") {
|
|
56
|
+
const prefix = shebang ? `${shebang}\n` : "";
|
|
57
|
+
return `${prefix}process.stderr.write(${JSON.stringify(`${buildIdentityLine(identity)}\n`)});`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { build } from "esbuild";
|
|
3
|
+
import { buildBanner, computeRuntimeSourceSha256 } from "./build-info.mjs";
|
|
4
|
+
|
|
5
|
+
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
|
|
6
|
+
const sourceSha256 = await computeRuntimeSourceSha256();
|
|
7
|
+
const identity = (target) => ({
|
|
8
|
+
packageName: packageJson.name,
|
|
9
|
+
version: packageJson.version,
|
|
10
|
+
target,
|
|
11
|
+
sourceSha256,
|
|
12
|
+
});
|
|
13
|
+
const common = {
|
|
14
|
+
bundle: true,
|
|
15
|
+
platform: "node",
|
|
16
|
+
format: "esm",
|
|
17
|
+
target: "node22.19",
|
|
18
|
+
};
|
|
19
|
+
const coreExternals = [
|
|
20
|
+
"@ctliz/agent-intercom-core",
|
|
21
|
+
"@ctliz/agent-intercom-core/*",
|
|
22
|
+
];
|
|
23
|
+
const outputs = [
|
|
24
|
+
{ entry: "codex/server.ts", outfile: "dist/codex-server.mjs", target: "codex-server", executable: true },
|
|
25
|
+
{ entry: "broker/broker.ts", outfile: "dist/broker.mjs", target: "broker", executable: false },
|
|
26
|
+
{ entry: "codex/bridge-daemon.ts", outfile: "dist/bridge-daemon.mjs", target: "bridge-daemon", executable: true },
|
|
27
|
+
{ entry: "codex/coi.ts", outfile: "dist/coi.mjs", target: "coi", executable: true, external: ["codex", "node-pty"] },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
await Promise.all(outputs.map((output) => build({
|
|
31
|
+
...common,
|
|
32
|
+
entryPoints: [output.entry],
|
|
33
|
+
outfile: output.outfile,
|
|
34
|
+
banner: { js: buildBanner(identity(output.target), output.executable ? "#!/usr/bin/env node" : "") },
|
|
35
|
+
external: [...coreExternals, ...(output.external ?? [])],
|
|
36
|
+
})));
|
|
37
|
+
|
|
38
|
+
await writeFile("dist/build-info.json", `${JSON.stringify({
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
package: packageJson.name,
|
|
41
|
+
version: packageJson.version,
|
|
42
|
+
sourceSha256,
|
|
43
|
+
targets: outputs.map((output) => output.target),
|
|
44
|
+
}, null, 2)}\n`, { mode: 0o644 });
|
|
45
|
+
|
|
46
|
+
for (const output of outputs) {
|
|
47
|
+
const built = await readFile(output.outfile, "utf8");
|
|
48
|
+
if (!built.includes(sourceSha256) || !built.includes(`target=${output.target}`)) {
|
|
49
|
+
throw new Error(`Built output omitted immutable identity: ${output.outfile}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await Promise.all(outputs
|
|
54
|
+
.filter((output) => output.executable)
|
|
55
|
+
.map((output) => chmod(output.outfile, 0o755)));
|