super-dsh 0.1.10-e → 0.1.11-a

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.
Files changed (50) hide show
  1. package/README.md +38 -2
  2. package/agent-agy/dist/agent.js +19 -1
  3. package/agent-claude/dist/agent.js +23 -5
  4. package/agent-codex/dist/adapter.js +3 -2
  5. package/agent-codex/dist/agent.js +41 -7
  6. package/agent-codex/dist/app-server-client.js +9 -2
  7. package/agent-codex/dist/app-server-protocol.js +6 -2
  8. package/agent-codex/dist/app-server-wire.js +29 -24
  9. package/agent-codex/dist/codex-launch.js +10 -32
  10. package/agent-codex/dist/codex-native-config.js +9 -3
  11. package/agent-codex/dist/codex-oauth.js +153 -0
  12. package/agent-codex/dist/codex-provider-gateway.js +36 -38
  13. package/agent-codex/dist/codex-providers.js +20 -7
  14. package/agent-codex/dist/codex-runtime-fetch.js +293 -0
  15. package/agent-codex/dist/codex-runtime.js +120 -0
  16. package/agent-codex/dist/index.js +20 -12
  17. package/agent-codex/package.json +8 -3
  18. package/agent-codex-sdk/dist/adapter.js +3 -2
  19. package/agent-codex-sdk/dist/agent.js +35 -1
  20. package/agent-codex-sdk/dist/codex-native-config.js +9 -3
  21. package/agent-codex-sdk/dist/codex-oauth.js +153 -0
  22. package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
  23. package/agent-codex-sdk/dist/codex-providers.js +20 -7
  24. package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
  25. package/agent-codex-sdk/dist/index.js +18 -11
  26. package/agent-codex-sdk/dist/sdk-runtime.js +3 -3
  27. package/agent-hermes/dist/agent.js +46 -2
  28. package/agent-hermes/dist/hermes-client.js +49 -1
  29. package/agent-omp/dist/agent.js +17 -4
  30. package/agent-omp-sdk/dist/agent.d.ts +11 -0
  31. package/agent-omp-sdk/dist/agent.js +17 -4
  32. package/agent-pi/dist/agent.js +33 -1
  33. package/client/index.js +10 -7
  34. package/package.json +7 -5
  35. package/superd-hub/dist/client/agent-settings.d.ts +1 -1
  36. package/superd-hub/dist/client/agent-settings.js +2 -2
  37. package/superd-hub/dist/connection-trust.d.ts +8 -0
  38. package/superd-hub/dist/connection-trust.js +16 -0
  39. package/superd-hub/dist/web-trust.d.ts +14 -42
  40. package/superd-hub/dist/web-trust.js +18 -43
  41. package/superd-hub/dist/world-mount.d.ts +6 -5
  42. package/superd-hub/dist/world-mount.js +6 -6
  43. package/superd-hub/lib/client/index.js +10 -7
  44. package/superd-hub/package.json +5 -1
  45. package/agent-codex/README.md +0 -179
  46. package/agent-codex-sdk/README.md +0 -79
  47. package/agent-hermes/README.md +0 -145
  48. package/agent-omp/README.md +0 -94
  49. package/agent-omp-sdk/README.md +0 -103
  50. package/agent-pi/README.md +0 -67
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Use the native index authenticator as a cookie-only admission delegate.
3
+ * A fixed, query-free index path prevents token exchange or redirects here;
4
+ * original Host and Cookie headers preserve the native authority binding and
5
+ * lifetime. No upstream private fields or cookie-format copies are needed.
6
+ */
7
+ export function installAuthenticationOnlyAdmission(connection) {
8
+ const original = connection.requestRejection;
9
+ const response = { writeHead() { }, end() { } };
10
+ connection.requestRejection = (request) => connection.authorizeIndex({
11
+ headers: request.headers,
12
+ method: 'GET',
13
+ url: '/index.html',
14
+ }, response) ? undefined : 401;
15
+ return () => { connection.requestRejection = original; };
16
+ }
@@ -1,41 +1,8 @@
1
1
  /**
2
- * Web page trust component — the browser-side twin of the connection row's
3
- * server-side `trustedHosts` fence (ported from better-dsh `web-trust.ts`,
4
- * authorities leg only).
5
- *
6
- * Why: when the Web UI is served on an operator-declared non-loopback
7
- * authority (Caddy domain, LAN IP), the server side accepts the Host through
8
- * `trustedHosts`, but the BROWSER-side connection client computes
9
- * `ctx.connection.isLoopback` from `location.hostname` — non-loopback → the
10
- * page degrades into the describe-mirror shape (Host settings persistence
11
- * reports terminal `memory` unavailability). This component injects one
12
- * inline `<head>` script through the webserver's public
13
- * `webserver/index-inject` event (every index render re-emits; row data is
14
- * read fresh at emit time, so a config reload needs no listener re-wiring):
15
- * when the page's hostname is one the operator declared their own, it sets
16
- * `window.__DSH_TRANSPORT__ = { ownsHost: true }` BEFORE any application
17
- * bundle materializes — the connection client's only whole-repo consumer —
18
- * restoring the upstream-documented shape for trusted authorities.
19
- *
20
- * The object deliberately carries NO fetch/openStream: the connection
21
- * client's optional chaining then builds the default HTTP/WS transport, so
22
- * the flag's only effect is the loopback verdict. An existing
23
- * `__DSH_TRANSPORT__` (a worker shell that owns a real transport) is never
24
- * overwritten.
25
- *
26
- * On/off (two independent levels, both standard):
27
- * - row level: a profile layer with `- id: superd-web-trust / disabled: true`
28
- * removes the component entirely;
29
- * - config level: `trustedPageAuthorities: []` keeps the row mounted but
30
- * injects nothing (fully inert).
31
- *
32
- * Composed on ctx0 only (since path-separation T-D): the `super-dsh/web-trust`
33
- * row in the composite bundle's insert list. The per-world row retired with
34
- * the world pages themselves (T-D disabled the world webui rows — no world
35
- * page renders, so no world-local injection exists); `/hub` is the sole
36
- * foreign entry and it serves ctx0's index, which this ctx0 row covers.
37
- *
38
- * @module @pgmi-builds/superd-hub/web-trust
2
+ * Browser ownership and opt-in authentication-only server admission.
3
+ * trustAllHosts skips Host/Origin restrictions while retaining native cookie
4
+ * authentication. Existing transports and declared-authority mode are preserved.
5
+ * Mounted on ctx0, whose connection also gates the /hub routes.
39
6
  */
40
7
  import type { Context } from '@deepseek-ai/cordis';
41
8
  import z from '@deepseek-ai/schemastery';
@@ -70,19 +37,23 @@ export interface BootScriptRow {
70
37
  * inert-when-empty, hostile-string escaping).
71
38
  *
72
39
  * @param trustedPageAuthorities - the operator-declared hostnames.
73
- * @returns the script text, or `undefined` when the list is empty (inject
74
- * nothing — the component is fully inert).
40
+ * @param trustAllHosts - enable browser ownership for every hostname.
41
+ * @returns script text, or undefined for an empty declared-authority list.
75
42
  */
76
- export declare function buildTrustScript(trustedPageAuthorities: readonly string[] | undefined): string | undefined;
43
+ export declare function buildTrustScript(trustedPageAuthorities: readonly string[] | undefined, trustAllHosts?: boolean): string | undefined;
77
44
  /** Row config schema. */
78
45
  export interface TrustConfig {
46
+ /** Skip address fences on server and browser; native authentication remains. */
47
+ trustAllHosts?: boolean;
79
48
  /** Hostnames this operator declares their own devices' pages run on. */
80
49
  trustedPageAuthorities?: readonly string[];
81
50
  }
82
51
  /** Row config schema (schema-level default derives from the trusted-hosts env). */
83
52
  export declare const Config: z<Schemastery.ObjectS<NoInfer<{
53
+ trustAllHosts: z<boolean, boolean, "defined">;
84
54
  trustedPageAuthorities: z<string[], string[], "defined">;
85
55
  }>>, Schemastery.ObjectT<NoInfer<{
56
+ trustAllHosts: z<boolean, boolean, "defined">;
86
57
  trustedPageAuthorities: z<string[], string[], "defined">;
87
58
  }>>, "plain">;
88
59
  /** Cordis plugin name. */
@@ -93,8 +64,7 @@ export declare const name = "superd-web-trust";
93
64
  */
94
65
  export declare const inject: string[];
95
66
  /**
96
- * Mount the authorities-leg injection: one `webserver/index-inject` listener
97
- * pushing the rendered row.
67
+ * Mount browser ownership and, when opted in, native authentication-only admission.
98
68
  * @param ctx - host plugin context.
99
69
  * @param config - the row config.
100
70
  */
@@ -103,8 +73,10 @@ declare const _default: {
103
73
  name: string;
104
74
  inject: string[];
105
75
  Config: z<Schemastery.ObjectS<NoInfer<{
76
+ trustAllHosts: z<boolean, boolean, "defined">;
106
77
  trustedPageAuthorities: z<string[], string[], "defined">;
107
78
  }>>, Schemastery.ObjectT<NoInfer<{
79
+ trustAllHosts: z<boolean, boolean, "defined">;
108
80
  trustedPageAuthorities: z<string[], string[], "defined">;
109
81
  }>>, "plain">;
110
82
  apply: typeof apply;
@@ -1,43 +1,11 @@
1
1
  /**
2
- * Web page trust component — the browser-side twin of the connection row's
3
- * server-side `trustedHosts` fence (ported from better-dsh `web-trust.ts`,
4
- * authorities leg only).
5
- *
6
- * Why: when the Web UI is served on an operator-declared non-loopback
7
- * authority (Caddy domain, LAN IP), the server side accepts the Host through
8
- * `trustedHosts`, but the BROWSER-side connection client computes
9
- * `ctx.connection.isLoopback` from `location.hostname` — non-loopback → the
10
- * page degrades into the describe-mirror shape (Host settings persistence
11
- * reports terminal `memory` unavailability). This component injects one
12
- * inline `<head>` script through the webserver's public
13
- * `webserver/index-inject` event (every index render re-emits; row data is
14
- * read fresh at emit time, so a config reload needs no listener re-wiring):
15
- * when the page's hostname is one the operator declared their own, it sets
16
- * `window.__DSH_TRANSPORT__ = { ownsHost: true }` BEFORE any application
17
- * bundle materializes — the connection client's only whole-repo consumer —
18
- * restoring the upstream-documented shape for trusted authorities.
19
- *
20
- * The object deliberately carries NO fetch/openStream: the connection
21
- * client's optional chaining then builds the default HTTP/WS transport, so
22
- * the flag's only effect is the loopback verdict. An existing
23
- * `__DSH_TRANSPORT__` (a worker shell that owns a real transport) is never
24
- * overwritten.
25
- *
26
- * On/off (two independent levels, both standard):
27
- * - row level: a profile layer with `- id: superd-web-trust / disabled: true`
28
- * removes the component entirely;
29
- * - config level: `trustedPageAuthorities: []` keeps the row mounted but
30
- * injects nothing (fully inert).
31
- *
32
- * Composed on ctx0 only (since path-separation T-D): the `super-dsh/web-trust`
33
- * row in the composite bundle's insert list. The per-world row retired with
34
- * the world pages themselves (T-D disabled the world webui rows — no world
35
- * page renders, so no world-local injection exists); `/hub` is the sole
36
- * foreign entry and it serves ctx0's index, which this ctx0 row covers.
37
- *
38
- * @module @pgmi-builds/superd-hub/web-trust
2
+ * Browser ownership and opt-in authentication-only server admission.
3
+ * trustAllHosts skips Host/Origin restrictions while retaining native cookie
4
+ * authentication. Existing transports and declared-authority mode are preserved.
5
+ * Mounted on ctx0, whose connection also gates the /hub routes.
39
6
  */
40
7
  import z from '@deepseek-ai/schemastery';
8
+ import { installAuthenticationOnlyAdmission } from './connection-trust.js';
41
9
  /**
42
10
  * The schema-level default for `trustedPageAuthorities` (better-dsh v0.2.2a
43
11
  * shape): derive from the trusted-hosts environment — the SAME single source
@@ -76,10 +44,12 @@ function frame(body) {
76
44
  * inert-when-empty, hostile-string escaping).
77
45
  *
78
46
  * @param trustedPageAuthorities - the operator-declared hostnames.
79
- * @returns the script text, or `undefined` when the list is empty (inject
80
- * nothing — the component is fully inert).
47
+ * @param trustAllHosts - enable browser ownership for every hostname.
48
+ * @returns script text, or undefined for an empty declared-authority list.
81
49
  */
82
- export function buildTrustScript(trustedPageAuthorities) {
50
+ export function buildTrustScript(trustedPageAuthorities, trustAllHosts = false) {
51
+ if (trustAllHosts)
52
+ return frame(['if(!window.__DSH_TRANSPORT__)window.__DSH_TRANSPORT__={ownsHost:true};']);
83
53
  const authorities = [...(trustedPageAuthorities ?? [])];
84
54
  if (authorities.length === 0)
85
55
  return undefined;
@@ -98,6 +68,7 @@ function trustedHostsEnv() {
98
68
  }
99
69
  /** Row config schema (schema-level default derives from the trusted-hosts env). */
100
70
  export const Config = z.object({
71
+ trustAllHosts: z.boolean().default(false),
101
72
  trustedPageAuthorities: z.array(String).default(deriveDefaultPageAuthorities(trustedHostsEnv())),
102
73
  });
103
74
  /** Cordis plugin name. */
@@ -108,16 +79,20 @@ export const name = 'superd-web-trust';
108
79
  */
109
80
  export const inject = [];
110
81
  /**
111
- * Mount the authorities-leg injection: one `webserver/index-inject` listener
112
- * pushing the rendered row.
82
+ * Mount browser ownership and, when opted in, native authentication-only admission.
113
83
  * @param ctx - host plugin context.
114
84
  * @param config - the row config.
115
85
  */
116
86
  export function apply(ctx, config) {
87
+ if (config?.trustAllHosts) {
88
+ ctx.inject(['connection'], owner => {
89
+ owner.effect(() => installAuthenticationOnlyAdmission(owner.connection), 'superd-web-trust: authentication-only admission');
90
+ });
91
+ }
117
92
  const authorities = config?.trustedPageAuthorities;
118
93
  ctx.inject(['webServer'], () => {
119
94
  ctx.on('webserver/index-inject', (table) => {
120
- const text = buildTrustScript(authorities);
95
+ const text = buildTrustScript(authorities, config?.trustAllHosts);
121
96
  if (text !== undefined)
122
97
  table.push({ kind: 'script', placement: 'head', text });
123
98
  });
@@ -93,13 +93,14 @@ export declare const WORLD_WAVE1_DISABLED_ROWS: readonly string[];
93
93
  * Wave-2 of world composition minimization: the CLIENT lane. The
94
94
  * multi-ctx-multi-ui → native-ui-only transition (0.1.5 / T-D) retired the
95
95
  * world's own web UI (webserver/hmr/web-runtime/client-hmr above); wave-2
96
- * completes it by stripping the browser-half rows of the web-app bundle —
97
- * every dsh-client-* row plus the cordis client runner. In a world these
98
- * only registered modules into a table no browser ever reads.
96
+ * completes it by stripping the browser-only rows of the web-app bundle
97
+ * plus the cordis client runner. A dsh-client-* package can also provide
98
+ * host services, so its name alone does not make it safe to disable.
99
99
  *
100
100
  * Dependency-cleared 2026-09-27: the KEEP side (api controllers, gateway,
101
- * session core, adapters) injects none of their services with ONE
102
- * exceptions — `connection` (dsh-client-connection: the world's typert
101
+ * session core, adapters) keeps the required host services:
102
+ * `file-upload` (dsh-client-file-upload: sessionController injects its
103
+ * fileUploads service), `connection` (dsh-client-connection: the world's typert
103
104
  * gateway injects it) and `modules` (dsh-client-modules: its activation
104
105
  * work fails in world trees ("client bundles not found") and at least one
105
106
  * fiber treats it as required — the load-bearing parking restatement
@@ -95,13 +95,14 @@ export const WORLD_WAVE1_DISABLED_ROWS = [
95
95
  * Wave-2 of world composition minimization: the CLIENT lane. The
96
96
  * multi-ctx-multi-ui → native-ui-only transition (0.1.5 / T-D) retired the
97
97
  * world's own web UI (webserver/hmr/web-runtime/client-hmr above); wave-2
98
- * completes it by stripping the browser-half rows of the web-app bundle —
99
- * every dsh-client-* row plus the cordis client runner. In a world these
100
- * only registered modules into a table no browser ever reads.
98
+ * completes it by stripping the browser-only rows of the web-app bundle
99
+ * plus the cordis client runner. A dsh-client-* package can also provide
100
+ * host services, so its name alone does not make it safe to disable.
101
101
  *
102
102
  * Dependency-cleared 2026-09-27: the KEEP side (api controllers, gateway,
103
- * session core, adapters) injects none of their services with ONE
104
- * exceptions — `connection` (dsh-client-connection: the world's typert
103
+ * session core, adapters) keeps the required host services:
104
+ * `file-upload` (dsh-client-file-upload: sessionController injects its
105
+ * fileUploads service), `connection` (dsh-client-connection: the world's typert
105
106
  * gateway injects it) and `modules` (dsh-client-modules: its activation
106
107
  * work fails in world trees ("client bundles not found") and at least one
107
108
  * fiber treats it as required — the load-bearing parking restatement
@@ -110,7 +111,6 @@ export const WORLD_WAVE1_DISABLED_ROWS = [
110
111
  export const WORLD_WAVE2_DISABLED_ROWS = [
111
112
  'cordis-client-runner',
112
113
  'directory-picker-browse-surface',
113
- 'file-upload',
114
114
  'locale',
115
115
  'resources',
116
116
  'ui-agent-preset', 'ui-approval', 'ui-attachment', 'ui-brand-official',
@@ -1738,14 +1738,14 @@ function modelOption(raw) {
1738
1738
  };
1739
1739
  }
1740
1740
  function providerAuthKind(raw) {
1741
- return raw === "env" ? "env" : raw === "authToken" ? "authToken" : "apiKey";
1741
+ return raw === "oauth" ? "oauth" : raw === "env" ? "env" : raw === "authToken" ? "authToken" : "apiKey";
1742
1742
  }
1743
1743
  function providerOption(raw) {
1744
1744
  if (typeof raw !== "object" || raw === null) return void 0;
1745
1745
  const record = raw;
1746
1746
  const id = nonEmpty(record.id);
1747
1747
  const label = nonEmpty(record.label);
1748
- const baseUrl = nonEmpty(record.baseUrl);
1748
+ const baseUrl = nonEmpty(record.baseUrl) ?? (record.authKind === "oauth" ? "" : void 0);
1749
1749
  if (id === void 0 || label === void 0 || baseUrl === void 0) return void 0;
1750
1750
  const models = Array.isArray(record.models) ? record.models.map(modelOption).filter((entry) => entry !== void 0) : [];
1751
1751
  const envName = nonEmpty(record.envName);
@@ -2650,6 +2650,10 @@ function ProvidersPanel({ agent, namespace }) {
2650
2650
  style: chip,
2651
2651
  children: "token"
2652
2652
  }),
2653
+ provider.authKind === "oauth" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2654
+ style: chip,
2655
+ children: "native OAuth"
2656
+ }),
2653
2657
  !provider.hasKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2654
2658
  style: chip,
2655
2659
  children: "no key"
@@ -2668,13 +2672,13 @@ function ProvidersPanel({ agent, namespace }) {
2668
2672
  }),
2669
2673
  provider.models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2670
2674
  style: chipWarn,
2671
- children: "no models yet — add one"
2675
+ children: provider.authKind === "oauth" ? "model cache unavailable — open Codex to refresh" : "no models yet — add one"
2672
2676
  }),
2673
2677
  busy === `remove:${provider.id}` && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2674
2678
  style: chip,
2675
2679
  children: "removing…"
2676
2680
  }),
2677
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2681
+ provider.authKind !== "oauth" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2678
2682
  type: "button",
2679
2683
  style: buttonStyle,
2680
2684
  disabled: busy !== null,
@@ -2682,8 +2686,7 @@ function ProvidersPanel({ agent, namespace }) {
2682
2686
  openEdit(provider);
2683
2687
  },
2684
2688
  children: "Edit"
2685
- }),
2686
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2689
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2687
2690
  type: "button",
2688
2691
  style: {
2689
2692
  ...buttonStyle,
@@ -2695,7 +2698,7 @@ function ProvidersPanel({ agent, namespace }) {
2695
2698
  run(`remove:${provider.id}`, () => removeProvider(agent.key, namespace, provider.id));
2696
2699
  },
2697
2700
  children: "Delete"
2698
- })
2701
+ })] })
2699
2702
  ]
2700
2703
  }, provider.id)),
2701
2704
  editing === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -43,7 +43,8 @@
43
43
  "@deepseek-ai/dsh-home-paths": ">=0.1.5-0 <0.3.0-0",
44
44
  "@deepseek-ai/dsh-host-webserver": ">=0.1.5-0 <0.3.0-0",
45
45
  "@deepseek-ai/dsh-tools": ">=0.1.5-0 <0.3.0-0",
46
- "@deepseek-ai/dsh-typert-protocol": ">=0.1.5-0 <0.3.0-0"
46
+ "@deepseek-ai/dsh-typert-protocol": ">=0.1.5-0 <0.3.0-0",
47
+ "@deepseek-ai/dsh-client-connection": ">=0.1.5-0 <0.3.0-0"
47
48
  },
48
49
  "peerDependenciesMeta": {
49
50
  "@deepseek-ai/cordis": {
@@ -63,6 +64,9 @@
63
64
  },
64
65
  "@deepseek-ai/dsh-typert-protocol": {
65
66
  "optional": true
67
+ },
68
+ "@deepseek-ai/dsh-client-connection": {
69
+ "optional": true
66
70
  }
67
71
  },
68
72
  "devDependencies": {
@@ -1,179 +0,0 @@
1
- # @pgmi-builds/agent-codex
2
-
3
- superd 外壳下的 **Codex adapter**(super-dsh 线):把 Codex CLI 经由 **`codex app-server`**
4
- 接进 DSH —— 会话、事件、模型目录、审批、持久化全部按 DSH 语义呈现,WebUI 消费的是 DSH
5
- 服务提供的数据面。
6
-
7
- > 通用规则(定位 / 两形态 / 测试准则 / 接线域 / per-agent 定制权 / home 与 profile 布局)
8
- > 见上级目录的 `agent-adapter-dev-rules.md` —— 改本包前先读它。
9
- > 迁移计划与实证:`docs/superpowers/plans/2026-10-03-agent-codex-app-server-migration.md`。
10
-
11
- ## 它是什么
12
-
13
- - 一个**普通 dsh plugin bundle**:`package.json` 的 `dsh.bundle.patch` 指向本目录的
14
- `cordis.patch.yml`(挂 codex provider、关掉它替代的原生行、重写权限三档表、pin 应用内目录选择器)。
15
- - 一个 **AgentFactory**:`src/index.ts` 里的 `CodexProvider` 在 `ctx.agents` 注册工厂,
16
- **每个会话一个 `codex app-server` 子进程**(`src/app-server-client.ts`);
17
- `src/agent.ts` 把 app-server 通知流投影成 DSH 会话事件。
18
- - **不包含** 任何 hub 耦合:世界挂载由 hub 侧驱动。本包自己就足以点亮一个完整的 web app。
19
-
20
- ## 线别(2026-10-03 迁移)
21
-
22
- **app-server 线**取代了原 `@openai/codex-sdk` 线(SDK 依赖已从本包与主 pack 移除)。
23
- SDK 线在 adapter 里登记的一整排降级被 app-server 协议面直接消掉:
24
-
25
- | 能力 | SDK 线(已退役) | app-server 线(现役) |
26
- |---|---|---|
27
- | 打断 | 只有 AbortSignal(掐断本轮 stream) | **`turn/interrupt`** |
28
- | steer | 无 → 排队 followUp 近似 | **`turn/steer`**(注入进行中的 turn) |
29
- | 运行时换模型 | 无 → 下一轮 `resumeThread` | **`turn/start.model`**(下一轮起生效,线程不重建) |
30
- | 运行时审批档 | 无 → launch-only 预设 | **`turn/start.approvalPolicy`**(折叠 `approval/policy`,`/permission` 可用) |
31
- | 运行时沙箱档 | 无 → launch-only | **`turn/start.sandboxPolicy`**(折叠 `sandbox/mode`) |
32
- | 审批人机回路 | 无(`sendRaw` 是 no-op) | **`*/requestApproval` → DSH `ctx.approval`**(Web UI ApprovalPanel) |
33
- | 用户消息回显 | 无(SDK item 无 `user_message`) | `item/started userMessage` |
34
- | token usage | 只在本轮 `turn/completed`,滞后 | `thread/tokenUsage/updated`(本轮 `turn_end` 附着) |
35
-
36
- **未接(登记 backlog)**:`thread/compact/start`(`/compact` 仍显式拒绝)、
37
- `thread/list` 会话清单(DSH 日志仍是权威)、`skills/list` / `mcpServerStatus/list` 面板、
38
- `thread/fork`、`requestUserInput` / `mcpServer/elicitation`(fail-closed 应答)、
39
- collab/subagent 项。
40
-
41
- ## 单独跑起来(首选开发/验收形态)
42
-
43
- 场景级验证唯一入口 = **整包 pack 线**(`super-dsh/test/start-4999.sh`)+ dev3 目标机实装
44
- (2026-09-24「场景退役」裁决);本包不再有 standalone 启动脚本。
45
-
46
- ## Home 与 profile 布局(user 裁定)
47
-
48
- | 位置 | 内容 |
49
- |---|---|
50
- | `$DSH_HOME`(测试 = `<repo>/.tests`) | 测试 DSH home |
51
- | 原生 `~/.codex`(`CODEX_HOME` 才重定向) | **Codex 原生 app home**:`config.toml` / `auth.json` / 模型 catalog / rollout / state。adapter **绝不重定向**(红线 §5),**从不写入**(单向阀) |
52
- | `$DSH_HOME/agents/codex` | 该 world 的 **DSH home**:DSH 会话日志副本、storages、`dsh-sessions.json` 映射 |
53
- | `$DSH_HOME/profiles/superd-pub` | 整包线的 dsh profile(tarball 物理安装) |
54
-
55
- ## 关键实现点
56
-
57
- - **lazy spawn(硬门)**:`CodexAppServerClient.create()` 零 IO、零子进程;`ensureStarted()`
58
- 在**首个真实 prompt** 才 spawn + `initialize` + `thread/start|resume`。查看/创建/resume
59
- 一个会话不产生任何 native 进程。单测直接钉住这条不变量(transport factory 调用计数 = 0)。
60
- - **进程回收**:子进程 `detached: true`(自有进程组),close 时先 `SIGTERM` 进程组、
61
- 宽限后 `SIGKILL`,再 `child.kill()` 兜底——stdio MCP 后代 `setsid` 逃离时仍能被扫到。
62
- - **session 身份 = 一对一映射**:DSH session id 是权威;Codex thread id 在**线程 attach**
63
- 时经 `onThreadAttached` 写入 `<dshHome>/dsh-sessions.json`(`src/session-map.ts`)。
64
- resume 只经该映射重挂线程,查不到即 fail-closed。
65
- - **DSH-native 持久化**:工厂按上游 agent-loop 的方式持有写通道(create `persistence.create` /
66
- resume `persistence.open(id,'write')` → `read` → `interruptedTurnClosers` → `prepare`),
67
- 发布点刷未存后缀,dispose 时 `handle.close()` 排空。
68
- - **每会话路由钉住**:create/resume 时同步钉住本会话路由(追加 `model/selection` +
69
- `installModelSelection`),**不依赖全局默认模型的落盘时序**。
70
- - **每轮权限折叠**:`agent.ts` 在每次 `turn/start` 前把会话日志的
71
- `approval/policy` / `sandbox/mode`(无则由 preset 表兜底)折成 codex 覆盖
72
- (`permission.ts` 的 `turnPolicyFromEvents`);`CODEX_APPROVAL_MODE` 只覆盖审批半边。
73
- - **审批桥 fail-closed**:`app-server-approvals.ts` 把 server-request 映射到
74
- `ctx.approval.request()`;无 answerer / 抛错 / 超时(`CODEX_APPROVAL_TIMEOUT_MS`,默认 5 分钟)
75
- 一律 **decline**,绝不让 codex 挂死。
76
- - **事件映射**:`system/message`(rollout HEAD `base_instructions`)、`request/context`、
77
- `assistant/message`(reasoning 用 `{type:'reasoning',text}`)、
78
- `tool/call`(原始 JSON 串)/`tool/result`、`step/start|end`、`turn/start|end`、`todo/write`。
79
-
80
- ## 环境变量
81
-
82
- | 变量 | 作用 |
83
- |---|---|
84
- | `CODEX_TRACE=1` | provider/agent/app-server 追踪(stderr) |
85
- | `CODEX_BIN` | 覆盖 codex 入口脚本(测试);默认 = 本包 pin 的 `@openai/codex/bin/codex.js` |
86
- | `CODEX_HOME` | Codex 原生 home 重定向(**测试专用**;生产路径零重定向) |
87
- | `CODEX_APPROVAL_MODE` | 覆盖审批档(`never`/`on-request`/`on-failure`/`untrusted`;非法值忽略) |
88
- | `CODEX_APPROVAL_TIMEOUT_MS` | 审批应答超时;`0` 关闭;默认 300000 |
89
- | `CODEX_IDLE_EXIT_MS` | 会话空闲后拆客户端;`0` 关闭;默认 600000 |
90
-
91
- ## 测试
92
-
93
- ```bash
94
- npm run build # tsc strict
95
- DSH_HOME=<repo>/.tests \
96
- SUPERD_DSH_ANCHOR=<repo>/upstream/deepseek-harness/package.json \
97
- node --test test/*.test.mjs
98
- ```
99
-
100
- 单测全部是 fake:`test/fixtures/fake-app-server.mjs` 是一个**可编程的 app-server 替身**
101
- (真 JSONL JSON-RPC 分帧、真子进程),wire / client 测试用真管道驱动它;投影器与审批桥是纯函数。
102
- **不 spawn 真 codex、不打模型。**
103
-
104
- ## 已知边界(app-server 线 v1)
105
-
106
- - `thread/compact/start` 未接 → `/compact` 仍以显式错误拒绝;
107
- - 会话清单仍由 DSH 日志提供(不接 `thread/list`,遵守 mapping-only);
108
- - `requestUserInput` / `mcpServer/elicitation` / `permissions/requestApproval` 一律 fail-closed;
109
- - collab / subagent / `thread/fork` / skills / MCP 面板未接;
110
- - 内存镜像 `getMessages()` 仅供诊断,权威转录是 DSH 会话日志。
111
-
112
- ## 多 provider 面(Hub + DSH,2026-10-04 重做)
113
-
114
- Codex 是**单 provider** harness:它自己的 `config.toml` 只能写一个 `model_provider`。provider 的**清单**
115
- 在 Codex 家里无处安放——那正是 cc-switch 存在的理由;而「这个会话现在跑在哪个 provider 上」在 DSH
116
- 里已有公认答案(`session/selectModel` + 持久的 `model/selection`)。所以本线**只做清单**,选择交给 DSH:
117
-
118
- - adapter 的 store 是**正本**(canonical),原生 `config.toml` 是**来源**(source),单向读入;
119
- - UI 不再有「active provider」概念——界面里切换 provider/model 就是 DSH 自带模型选择器,与本仓其它
120
- harness 完全一致;
121
- - provider 由**进程参数**注入子进程(`-c model_providers.<id>.*`),**不写任何原生文件**。
122
-
123
- ### 分账
124
-
125
- | 位置 | 内容 |
126
- |---|---|
127
- | `$DSH_HOME/agents/codex/codex-providers.json`(0600) | **正本**:provider 清单 + 被显式删除的 endpoint 墓碑(含 API key) |
128
- | `$DSH_HOME/agents/codex/codex-catalogs/<id>.json` | 本 adapter 自有的 per-provider 模型目录;经 `-c model_catalog_json=<绝对路径>` 注入 |
129
- | `~/.codex/config.toml` | 用户原生:**只读**。启动时 + 每 10 分钟读一次,按 endpoint URL 去重折进正本 |
130
- | `~/.codex/auth.json` | 用户原生:**只读**。`apiKey` 形态的 key 走子进程环境,不走这里 |
131
-
132
- ### 同步(2026-10-04 user 裁决)
133
-
134
- `syncFromNative()`:启动时 + 每 10 分钟(`SYNC_INTERVAL_MS = 600_000`)读 `config.toml` 的**全部**
135
- `[model_providers.<id>]` 表,按 endpoint URL 去重折进正本。新 endpoint 在库里没有名字,**就用 URL 端点
136
- 当名字**(`label = baseUrl`)——Codex 的 catalog 是「当前在役 provider」专用的,所以非在役 provider 的
137
- 模型清单**确实无从得知**,此时存 `models: []`(面板显示「no models yet」,该 provider 不注册路由),
138
- 而不是编一个。同步只**增/刷**、永不删;唯一的例外是用户显式删除留下的**墓碑**(endpoint key),
139
- 否则「删除」会在十分钟后自己撤销。
140
-
141
- ### 选择与切换
142
-
143
- - **路由 = DSH 的**:每个有模型的 provider 注册一条 `ctx.llm` 路由,于是 `session/modelCatalog` 天然
144
- 按 provider 分组,`session/selectModel` 天然持久(`model/selection` + `modelSelection` 投影);
145
- - **turn 边界判定 = DSH 的**:agent 每轮按 `pending ?? request/header ?? agentDefaultModel ?? options`
146
- 解析路由(`codex-route.ts`),并回写 `request/header`——于是投影的 `lastUsed`/`next`、
147
- `consumeSelection`、以及「会话上次跑在哪个 provider」全部由上游自洽,UI 重放自然准确;
148
- - **中途切换不用打断**:`turn/start` 的 `model` 覆盖「本轮及后续轮」,所以换**模型**是原生的;换
149
- **provider** 必须换子进程——`turn/start` 没有 provider 字段,而打给**已加载**线程的 `thread/resume`
150
- 是「接受但忽略」(2026-10-03 实测:要 `zhipu-cn` 回 `deepseek`)。故 agent 在**下一轮冷启动**时
151
- kill 掉旧 child、用新 `-c` 起新 child、`thread/resume` **同一个 thread id**,历史与上下文完整保留。
152
- turn 中发起的切换落在下一轮(DSH 的 pending 语义),**不打断在跑的 turn**。
153
-
154
- ### 启动注入(零原生写入)
155
-
156
- `codex-launch.ts` 生成 argv/env;三条实测约束写死在注释里:
157
-
158
- 1. hyphenated id 在 `-c` 点路径里合法(`model_providers.zhipu-cn.…`)⇒ 原生表名可原样作 provider id;
159
- 2. `model_catalog_json` 作为 override 时按**进程 cwd** 解析 ⇒ 路径必须绝对(相对路径实测 `ENOENT`);
160
- 3. `env_key` 是唯一不经过 auth manager 的形态 ⇒ 库里的 key 以合成变量名(`SUPERD_CODEX_KEY_<ID>`)
161
- 经**子进程环境**交给 codex,`auth.json` 完全不碰。**这也顺带解决了 ChatGPT 登录遮蔽 keyed provider
162
- 的老问题**(`requires_openai_auth` 才会被 `auth_mode=chatgpt` 遮蔽);只有「seeded」形态
163
- (`apiKey` 且 value 为空 = key 在 auth.json 里)才回落到 `requires_openai_auth`,面板仍会告警。
164
-
165
- ### RPC
166
-
167
- `TypertRemoteService`,namespace `codexProviders`(hub 经 `/hub/api?agent=codex` 原样转发):
168
- `get` / `save` / `remove`。**API key 只写不读**——wire 上只有 `hasKey`,绝不回传值。
169
- 没有 `select`:选择是 DSH 的 per-session 状态。
170
-
171
- ### 模型目录必须克隆模板
172
-
173
- codex 0.154 对 catalog 条目要求 `slug`/`display_name`/`supported_reasoning_levels`/`shell_type`/
174
- `visibility`/`supported_in_api`/`priority`/`support_verbosity` 以及**嵌套 struct** `truncation_policy`…
175
- 字段链未终止,故只从既有条目克隆并覆盖身份字段。目录是**替换式**:一份目录只写它那个 provider 的模型
176
- (2026-10-03 修正:早期「吸收且永不丢弃」让 Chat 模型清单只增不减)。
177
-
178
- 记录:`docs/superpowers/plans/2026-10-03-codex-multi-provider-hub.md` +
179
- `docs/test-reports/2026-10-04-codex-multi-provider-onthefly.md`。
@@ -1,79 +0,0 @@
1
- # @pgmi-builds/agent-codex-sdk
2
-
3
- superd 外壳下的 **Codex adapter**(super-dsh 线,AW-E):把 Codex CLI 经由
4
- `@openai/codex-sdk` 接进 DSH —— 会话、事件、模型目录、持久化全部按 DSH 语义呈现,
5
- WebUI 消费的是 DSH 服务提供的数据面。
6
-
7
- > 通用规则(定位 / 两形态 / 测试准则 / 接线域 / per-agent 定制权 / home 与 profile 布局)
8
- > 见上级目录的 `agent-adapter-dev-rules.md` —— 改本包前先读它。
9
-
10
- ## 它是什么
11
-
12
- - 一个**普通 dsh plugin bundle**:`package.json` 的 `dsh.bundle.patch` 指向本目录的
13
- `cordis.patch.yml`(挂 codex provider、关掉它替代的原生行、重写权限三档表、pin 应用内目录选择器)。
14
- - 一个 **AgentFactory**:`src/index.ts` 里的 `CodexProvider` 在 `ctx.agents` 注册工厂,
15
- 每个会话一个 `CodexSdkClient`(真 `@openai/codex-sdk` 线程);`src/agent.ts` 把 Codex 线程事件
16
- 投影成 DSH 会话事件。
17
- - **带自己的 world 入口**:`src/world-plugin.ts`(`./world`,world key `codex-sdk`)把它作为
18
- superd 世界挂载(`spawnWorld` + `worldMountPatches`),需要 hub 的 `@pgmi-builds/superd-hub`;
19
- 同一份 adapter 代码也可作为普通 bundle 点亮一个完整 web app(两形态一条代码路径)。
20
-
21
- ## 零声明依赖与运行时自取(2026-10-04 user 裁决)
22
-
23
- 本包 **不声明任何运行时依赖** —— `@openai/codex-sdk` / `@openai/codex` / `zod` 既不在
24
- `dependencies`、也不在 `optionalDependencies`、也不在 `peerDependencies`(`dependencies` 恒为
25
- `{}`),安装期零下载、零 postinstall。SDK 树在**首个真 prompt** 时才被 fetch 进
26
- `<worldHome>/sdk-runtime/`(`src/sdk-runtime.ts`):
27
-
28
- - marker `<worldHome>/sdk-runtime.json`(**最后写**:写不到就不算成功)、锁 `.lock`(mkdir 原子,
29
- 30 min 判陈旧可夺)、失败留 `.error.log`;
30
- - marker 身份 = 本包版本 + 两个 pin,任一变化即重取;命中且 entry 存在则零网络短路;
31
- - 装完立刻 smoke `import()` entry 并要求 `Codex` 构造器存在 —— 坏树在 fetch 时报错,不留到首 prompt;
32
- - 预热(dev/运维):`npm run fetch-sdk-runtime [<home>]`(只接受仓内 `.tests` 下的 home)。
33
-
34
- ## 跑起来
35
-
36
- 本包默认 **disabled**(composite 行 `agent-codex-sdk`,打开该行即启用):行为级验证只走整包
37
- pack 线(`super-dsh/test/start-4999.sh` + dev3 实装),不另起 per-agent 场景 rig(2026-09-24
38
- 场景退役,见 `agent-adapter-dev-rules.md` §2)。`CODEX_TRACE=1` 开 provider/agent 追踪。
39
- ## Home 与 profile 布局(user 裁定)
40
-
41
- | 位置 | 内容 |
42
- |---|---|
43
- | `$DSH_HOME`(测试 = `<repo>/.tests`) | 测试 DSH home |
44
- | `<DSH_HOME>/agents/codex-sdk` | 该世界的 **world DSH home**(`dsh-sessions.json` 映射等 adapter 态)+ **自取 SDK 运行时** `<worldHome>/sdk-runtime/`;codex app home 仍是原生 `~/.codex`(绝不重定向) |
45
- | `$DSH_HOME/profiles/codex-sdk` | 该世界的 dsh profile(bundles + `cordis.patch.yml`) |
46
- | `$DSH_HOME/sessions/...` | **DSH 会话日志**(上游 `session-persistence-jsonl`,zstd)——list/replay 都由此提供 |
47
-
48
- **禁止**自造 per-app home(如 `.tests/codex-app`)。
49
-
50
- ## 关键实现点
51
-
52
- - **session 身份 = 一对一映射**:DSH session id 是权威;Codex thread id 在其物化后写入
53
- `<worldHome>/dsh-sessions.json`(`src/session-map.ts`)。resume 只经该映射重挂线程,查不到即
54
- fail-closed。**不自建 rollout 扫描做 list,也不自解析 JSONL 做 replay。**
55
- - **DSH-native 持久化**:工厂按上游 agent-loop 的方式持有写通道(create `persistence.create` /
56
- resume `persistence.open(id,'write')` → `read` → `interruptedTurnClosers` → `prepare`),
57
- 发布点刷未存后缀,dispose 时 `handle.close()` 排空。
58
- - **每会话路由钉住**:工厂在 create/resume 时同步钉住本会话路由(追加 `model/selection` +
59
- `installModelSelection`),**不依赖全局默认模型的落盘时序**(共享 test home 里 settings 默认可能属别线)。
60
- - **事件映射**:`system/message`(Codex 系统提示,rollout HEAD `base_instructions`)、
61
- `request/context`(provider/model/contextWindow)、`assistant/message`(reasoning 用
62
- `{type:'reasoning',text}`)、`tool/call`(原始 JSON 串)/`tool/result`、`step/start|end`、`turn/start|end`。
63
-
64
- ## 测试
65
-
66
- ```bash
67
- npm run build # tsc strict
68
- DSH_HOME=<repo>/.tests \
69
- SUPERD_DSH_ANCHOR=<repo>/upstream/deepseek-harness/package.json \
70
- node --test test/*.test.mjs
71
- ```
72
-
73
- ## 已知边界(V1)
74
-
75
- - 审批仅 launch-only 预设(SDK 无运行时审批;app-server 线才有 `*/requestApproval`);
76
- - 无 steer / 无运行时换模型(下一轮生效)/ usage 滞后一轮 / 无 fork;
77
- - `compaction/*`、`todo/write`、`subagent/*` 未接(见规则文档 §9.4 backlog);
78
- - **world 入口**:`src/world-plugin.ts`(world key `codex-sdk`)已接线 `spawnWorld`;roster 条目
79
- fiber-scoped、行关闭时 dispose 世界(2026-10-03 omp-sdk 验收踩到的两条卸载契约)。