Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64310b3719 | |||
| f9ee0d5ac7 | |||
| 9070c0760b | |||
| 1e9d74fa4a | |||
| bdf7ed288a | |||
| 7f60a00c84 | |||
| 94200a3cdb | |||
| 286c402a4b | |||
| f30e6e9f33 | |||
| 7ecc9f4971 | |||
| 836d599a98 | |||
| 5ea453d5d1 | |||
| 08bca001a2 | |||
| e455cd593c | |||
| 38ae9d7d07 | |||
| abb3912530 | |||
| 91c333ea4a | |||
| ced4b56dee | |||
| 81a35e0bff | |||
| 647cf82cec | |||
| 080c3ebc4f | |||
| c75fcbf023 | |||
| cf5eac1c63 | |||
| ea81d8ebb5 | |||
| d0a8f254b3 | |||
| a312ee20b5 | |||
| 1a61c0bea7 | |||
| 7cd8b39593 | |||
| 5e7ff450b6 | |||
| 7fc739fad0 | |||
| d2366bc40a | |||
| 7400c706f8 | |||
| 6befa3c33a | |||
| 1746c77cb1 | |||
| 41874ab391 | |||
| f69d9a2e11 | |||
| dedff81475 | |||
| aa5221abcf | |||
| 9f5c84ee71 | |||
| 578f8019fc | |||
| 4d3f87cd39 | |||
| d901945c77 | |||
| d155e303bd | |||
| 02fff8f308 | |||
| 7774642365 | |||
| 8e78be766e | |||
| 7052d3e692 | |||
| 1b4cb3c45e | |||
| 15c227cebf | |||
| 21137f676b | |||
| f395a6ed36 | |||
| da7c4dc71c | |||
| fd423770d8 | |||
| 794f3d4cf8 | |||
| e04a8310a0 | |||
| 82957fe1d4 | |||
| 5e16837f3d | |||
| effdaefc63 | |||
| 8abeb333bb | |||
| 4c3c0e9cd4 | |||
| e3d28c50af | |||
| 8e3d9991fa | |||
| 51e35468a5 | |||
| 03a4410272 | |||
| e118d66699 | |||
| 1363b9374c | |||
| 3c457feb4c | |||
| aa5c857060 | |||
| 755bf80858 | |||
| 4c0fdf1806 | |||
| 7f129ddd6e | |||
| 4d79971d6b | |||
| 5550e24f83 | |||
| a4ead1a5d6 | |||
| 5949c2fd58 |
@@ -29,9 +29,10 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
|
||||
- All eight generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
|
||||
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID in generation requests. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
|
||||
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
|
||||
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
|
||||
|
||||
## Documentation Navigation
|
||||
|
||||
@@ -50,8 +50,8 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Capability | POST path | Required body fields | Common optional body fields |
|
||||
| --- | --- | --- | --- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
@@ -82,16 +82,18 @@ After confirming a local upload, pass its stable `objectKey` into operations tha
|
||||
| Target capability | Field |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceReferenceId` must be a registered project resource ID or asset ID; additional references remain in `referenceImageSrcs` |
|
||||
| Icon spritesheet | Register the primary spec as an `assetKind="icon-spec"` project resource or asset, then pass its returned ID as `referenceId`; additional style references remain in `referenceImageSrcs` |
|
||||
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Character animation | `sourceImageSrc` |
|
||||
| Video with image references | `referenceImageSrcs` |
|
||||
|
||||
Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
For image edit/redraw, confirming an upload is not sufficient: create a project resource or asset-library record first, then pass that record's ID as `sourceReferenceId`. The main source never accepts objectKey, URL, Data URL, or Blob URL. Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
|
||||
|
||||
## Common Values
|
||||
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
@@ -78,9 +78,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ A minimal `canvasCompletion` is:
|
||||
}
|
||||
```
|
||||
|
||||
`dialogId` is optional. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
`dialogId` is optional. The placeholder supplies canvas placement and completion coordinates; it is not a final media pixel-size constraint. For successful pixel-art snapping, the result layer uses the final logical-grid PNG dimensions even when they differ from the placeholder. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
|
||||
Character animation accepts `assetFolderId` and `assetLabel` and persists the final transparent sequence directly. Its completed compact result includes the authoritative `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`. Use those records directly and never synthesize a duplicate asset from the first frame.
|
||||
|
||||
@@ -145,7 +145,7 @@ Carry the current art spec in `generationInputs.artSpec` and reflect important c
|
||||
The top-level `style` field is not the art spec's visual-style prose. It appends a short server-side clause to the prompt sent to the provider and enables deterministic post-processing:
|
||||
|
||||
- Omitted, `null`, empty string, or `"none"`: no clause is appended and no post-processing runs, without warning.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. On successful snapping, each detected grid cell becomes one output pixel and the logical-grid PNG is persisted directly; it is not resized back to `size`, `imageSize`, the provider image, or `canvasCompletion.placeholder`. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- Unknown strings, or `"pixelArt"` on unsupported kinds such as `spec`, `quick-edit`, `ui-design`, or `publication-material`: continue without style processing and return `warning.code: "unsupported-image-style"`.
|
||||
- Non-string JSON values: malformed request, HTTP `400`.
|
||||
|
||||
@@ -169,6 +169,8 @@ client.generate_image(
|
||||
)
|
||||
```
|
||||
|
||||
Image edit/redraw has a stricter main-source identity rule. After upload confirmation, create either a project resource or an asset-library record and pass its `resourceId` or `assetId` as `sourceReferenceId`. Do not pass the uploaded objectKey as the main source; objectKey remains valid only for auxiliary `referenceImageSrcs` where the OpenAPI permits it.
|
||||
|
||||
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
|
||||
|
||||
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
|
||||
@@ -196,7 +198,7 @@ Do not guess dimensions or pass a temporary signed read URL. See `authentication
|
||||
|
||||
The completed `result` may contain stable artifact fields such as:
|
||||
|
||||
- `objectKey`, media type, dimensions, or task ID.
|
||||
- `objectKey`, media type, dimensions, or task ID. For successful `pixelArt`, image `width`/`height`, icon `spritesheetWidth`/`spritesheetHeight`, and nested resource/asset dimensions are the actual final logical-grid PNG dimensions rather than requested, provider, or placeholder dimensions.
|
||||
- Sound-effect `durationSeconds` is the probed MP3 duration and `loop` is the frozen request boolean; neither is inferred from Prompt text.
|
||||
- `resource`, `resourceId`, or equivalent canvas reference.
|
||||
- `asset`, `assetId`, or equivalent library reference.
|
||||
@@ -209,6 +211,10 @@ It deliberately excludes a complete project/canvas/library snapshot, Data URL, B
|
||||
|
||||
Interpret warnings only after the query reaches `status=completed`. The query-level `warning` is display-ready text. Compact `result.warning` and `result.sliceWarning` preserve structured artifact semantics.
|
||||
|
||||
### Delivery-size normalization result before pixel-art snapping
|
||||
|
||||
`result.warning.code: "dimension-restore-fallback"` records only the delivery-size normalization result established before any subsequent `pixelArt` snapping: the provider image could not be safely normalized, so its dimensions were preserved at that processing boundary. It does not describe or constrain the dimensions after `pixelArt`; if snapping succeeds, use the completed result's actual logical-grid dimensions as authoritative.
|
||||
|
||||
### Source-preserved post-processing failure
|
||||
|
||||
When `result.warning.code` is `postprocess-failed-source-preserved`:
|
||||
|
||||
@@ -548,13 +548,16 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def edit_image(self, prompt: str, source_image_src: str, **fields: Any) -> Any:
|
||||
def edit_image(self, prompt: str, source_reference_id: str, **fields: Any) -> Any:
|
||||
source_reference_id = source_reference_id.strip()
|
||||
if not source_reference_id:
|
||||
raise GenarrativeApiError("source_reference_id must be a registered resource or asset ID")
|
||||
self._apply_canvas_session_fields(fields, prompt, 1024, 1024)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/images/edits",
|
||||
{"prompt": prompt, "sourceImageSrc": source_image_src, **fields},
|
||||
{"prompt": prompt, "sourceReferenceId": source_reference_id, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,23 +69,8 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository lint gates
|
||||
run: npm run lint
|
||||
|
||||
- name: Build web applications
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content data
|
||||
run: npm run check:content
|
||||
|
||||
- name: Check committed whitespace
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
|
||||
test -n "${base_ref}"
|
||||
git cat-file -e "${base_ref}^{commit}"
|
||||
git diff --check "${base_ref}"...HEAD
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
|
||||
@@ -39,6 +39,8 @@ temp*build*/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
/apps/mobile-shell/.expo/
|
||||
/apps/mobile-shell/.expo-export-smoke/
|
||||
/apps/preview-deployer-web/dist/
|
||||
/apps/preview-deployer-web/node_modules/
|
||||
/server-rs/.spacetimedb/
|
||||
/server-rs/.data/
|
||||
/public/generated-animations
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npm run format:staged
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npm run check:pre-push-master -- "$@"
|
||||
@@ -26,6 +26,12 @@
|
||||
- 后续新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;历史文档不要求批量重命名,除非本次任务明确涉及。
|
||||
- 工程修改要同步更新对应 `docs/` 文档;产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则时,同步更新 `docs/project-memory/shared-memory/`。
|
||||
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件,不新建平行系统或平行页面。
|
||||
- 对已明确退役且不存在现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,坚持“四不写”:
|
||||
1. 不写历史兼容代码。
|
||||
2. 不写用于维持退役行为的防御性兼容测试。
|
||||
3. 不写仅说明其曾存在或已删除的墓碑注释。
|
||||
4. 不写仅记录其已删除的墓碑文档;直接将权威文档更新为当前状态。
|
||||
- 公开 API、持久化数据、SpacetimeDB schema、跨版本重放、活跃实例和正式迁移不适用“四不写”;必要兼容应最小化、白名单化并配套契约或迁移测试,迁移完成后同步删除兼容实现与对应测试。
|
||||
- UI 面板中不要默认写功能说明、规则描述或开发解释文案;移动端优先,同时保证网页端可正常显示和操作。
|
||||
- 点击按钮弹出独立面板的设计,不要实现成在当前面板下面追加内容。
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"agentMode": "codex_app_server",
|
||||
"llm": {
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"model": "gpt-4.1",
|
||||
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
||||
"model": "gpt-5.6-sol",
|
||||
"apiKind": "openai_responses",
|
||||
"reasoningEffort": "high",
|
||||
"stream": false,
|
||||
"reasoningEffort": "max",
|
||||
"stream": true,
|
||||
"webSearchEnabled": false,
|
||||
"contextWindowTokens": 128000,
|
||||
"autoCompactTokenLimit": 64000,
|
||||
@@ -15,9 +16,5 @@
|
||||
"retryBackoffMs": 500
|
||||
},
|
||||
"agentLlm": {},
|
||||
"editorApi": {
|
||||
"baseUrl": "http://127.0.0.1:8082",
|
||||
"apiKey": ""
|
||||
},
|
||||
"mcpServers": {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>AI 游戏创作</title>
|
||||
<title>陶泥儿</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+11
@@ -10,7 +10,9 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lexical": "^0.47.0",
|
||||
@@ -2208,6 +2210,15 @@
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-http": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz",
|
||||
"integrity": "sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat",
|
||||
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense",
|
||||
"agent-runtime:supervisor-game-chat-single-main-playable-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test",
|
||||
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
|
||||
@@ -36,7 +37,9 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lexical": "^0.47.0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2406,11 +2406,36 @@ export function isolatedJoinDeliveryTarget(delivery) {
|
||||
return target;
|
||||
}
|
||||
|
||||
export function finalMessageId(agentId, sessionId, runId) {
|
||||
const fingerprint = createHash('sha256')
|
||||
function runtimeMessageCorrelationId(agentId, sessionId, runId) {
|
||||
return createHash('sha256')
|
||||
.update(`${agentId}\n${sessionId}\n${runId}`)
|
||||
.digest('hex');
|
||||
return `agent-finalization-${fingerprint.slice(0, 32)}`;
|
||||
}
|
||||
|
||||
export function finalMessageId(agentId, sessionId, runId) {
|
||||
return `agent-finalization-${runtimeMessageCorrelationId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
).slice(0, 32)}`;
|
||||
}
|
||||
|
||||
export function runtimePublicStatusMessageId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
status,
|
||||
) {
|
||||
const correlationId = runtimeMessageCorrelationId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
).slice(0, 32);
|
||||
const statusFingerprint = createHash('sha256')
|
||||
.update(status)
|
||||
.digest('hex')
|
||||
.slice(0, 16);
|
||||
return `runtime-public-status-${correlationId}-${statusFingerprint}`;
|
||||
}
|
||||
|
||||
export function backgroundTaskMessageId(agentId, sessionId, runId, source) {
|
||||
@@ -2444,7 +2469,10 @@ export function disposableProjectPathVariants() {
|
||||
}
|
||||
|
||||
export function formalConfigPathVariants() {
|
||||
return absolutePathVariants(state.options?.configDir);
|
||||
return absolutePathVariants(
|
||||
state.options?.configDir,
|
||||
state.isolatedRunner?.appDataDir,
|
||||
);
|
||||
}
|
||||
|
||||
export function absolutePathVariants(...values) {
|
||||
|
||||
@@ -20,7 +20,15 @@ import {
|
||||
stopOwnedIsolatedRunner,
|
||||
} from './harness/app-data.mjs';
|
||||
import { loadConfig, parseArguments } from './harness/config.mjs';
|
||||
import { closeInteractiveCli } from './harness/process.mjs';
|
||||
import {
|
||||
activeInteractiveCliSessions,
|
||||
captureOwnedProcessCleanupSnapshot,
|
||||
closeInteractiveCli,
|
||||
destroyInteractiveCliOutputStreams,
|
||||
interactiveCliOutput,
|
||||
verifyOwnedProcessCleanupSnapshot,
|
||||
waitForInteractiveCliStdioClose,
|
||||
} from './harness/process.mjs';
|
||||
import { checkPrerequisites } from './harness/project.mjs';
|
||||
import {
|
||||
buildSummary,
|
||||
@@ -266,6 +274,21 @@ if (selfTestRequested) {
|
||||
recordError(error?.code ?? 'unexpected-error', error);
|
||||
} finally {
|
||||
state.cleanupInProgress = true;
|
||||
const stateTrackedInteractiveCliSessions = new Set(
|
||||
[
|
||||
state.userInputCliSession,
|
||||
state.supervisorAutonomousPlayableCliSession,
|
||||
state.supervisorSwarmCliSession,
|
||||
].filter(Boolean),
|
||||
);
|
||||
const interactiveCliSessions = [
|
||||
...new Set([
|
||||
...stateTrackedInteractiveCliSessions,
|
||||
...activeInteractiveCliSessions,
|
||||
]),
|
||||
];
|
||||
const supervisorAutonomousPlayableCliSession =
|
||||
state.supervisorAutonomousPlayableCliSession;
|
||||
if (isUserInputRuntimeSuite() && state.userInputCliSession) {
|
||||
try {
|
||||
await closeInteractiveCli(state.userInputCliSession);
|
||||
@@ -311,6 +334,15 @@ if (selfTestRequested) {
|
||||
}
|
||||
state.supervisorSwarmCliSession = null;
|
||||
}
|
||||
for (const session of interactiveCliSessions) {
|
||||
if (stateTrackedInteractiveCliSessions.has(session)) continue;
|
||||
try {
|
||||
await closeInteractiveCli(session);
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError('interactive-cli-cleanup-failed', error);
|
||||
}
|
||||
}
|
||||
if (isMcpRuntimeSuite() && state.mcp.httpFixture) {
|
||||
try {
|
||||
await stopMcpHttpFixture();
|
||||
@@ -320,16 +352,42 @@ if (selfTestRequested) {
|
||||
recordError('mcp-http-fixture-cleanup-failed', error);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
state.isolatedRunner.appDataDir
|
||||
) {
|
||||
try {
|
||||
const runnerPid = state.isolatedRunner.current?.pid ?? null;
|
||||
const helperPid =
|
||||
state.isolatedRunner.current?.killHandle?.child?.pid ?? null;
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot =
|
||||
await captureOwnedProcessCleanupSnapshot({
|
||||
runnerPid,
|
||||
helperPids: Number.isSafeInteger(helperPid) ? [helperPid] : [],
|
||||
rootPids: [
|
||||
...interactiveCliSessions.map((session) => session.child?.pid),
|
||||
...[...activeCommandChildren].map((child) => child.pid),
|
||||
].filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
||||
});
|
||||
const observed =
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot
|
||||
.observedCounts;
|
||||
assert(
|
||||
observed.runner === 1 && observed.helper === 1,
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-incomplete',
|
||||
);
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
|
||||
try {
|
||||
await stopOwnedIsolatedRunner();
|
||||
state.isolatedRunner.stopped = true;
|
||||
state.isolatedRunner.cleanupPerformed =
|
||||
await removeIsolatedSuiteAppData();
|
||||
if (!state.isolatedRunner.cleanupPerformed) {
|
||||
state.status = 'FAIL';
|
||||
recordError('isolated-appdata-cleanup-sentinel-missing');
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
const safeCleanupErrorCode =
|
||||
@@ -342,8 +400,51 @@ if (selfTestRequested) {
|
||||
state.isolatedRunner.current?.killHandle,
|
||||
).catch(() => {});
|
||||
}
|
||||
}
|
||||
for (const session of interactiveCliSessions) {
|
||||
try {
|
||||
await waitForInteractiveCliStdioClose(session, 10_000);
|
||||
} catch (error) {
|
||||
destroyInteractiveCliOutputStreams(session);
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
error?.code === 'interactive-cli-stdio-close-timeout'
|
||||
? error.code
|
||||
: 'interactive-cli-stdio-cleanup-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (supervisorAutonomousPlayableCliSession) {
|
||||
state.supervisorAutonomousPlayable.cliOutput = interactiveCliOutput(
|
||||
supervisorAutonomousPlayableCliSession,
|
||||
);
|
||||
}
|
||||
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
|
||||
if (state.isolatedRunner.stopped) {
|
||||
try {
|
||||
state.isolatedRunner.cleanupPerformed =
|
||||
await removeIsolatedSuiteAppData();
|
||||
if (!state.isolatedRunner.cleanupPerformed) {
|
||||
state.status = 'FAIL';
|
||||
recordError('isolated-appdata-cleanup-sentinel-missing');
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
const safeCleanupErrorCode =
|
||||
isNonEmptyString(error?.code) &&
|
||||
/^(?:isolated|source)-[a-z0-9-]+$/u.test(error.code)
|
||||
? error.code
|
||||
: 'isolated-appdata-cleanup-failed';
|
||||
recordError(safeCleanupErrorCode, error);
|
||||
}
|
||||
}
|
||||
const killMethod =
|
||||
state.isolatedRunner.pidfdClaimCount > 0 ? 'linux-pidfd' : null;
|
||||
state.isolatedRunner.pidfdClaimCount > 0
|
||||
? process.platform === 'win32'
|
||||
? 'windows-process-handle'
|
||||
: 'linux-pidfd'
|
||||
: null;
|
||||
if (isSteerRunnerKillSuite()) {
|
||||
state.evidence.steerRunnerStopped = state.isolatedRunner.stopped;
|
||||
state.evidence.steerAppDataCleanupPerformed =
|
||||
@@ -661,6 +762,61 @@ if (selfTestRequested) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
state.isolatedRunner.appDataDir
|
||||
) {
|
||||
const snapshot =
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot;
|
||||
if (snapshot) {
|
||||
try {
|
||||
const cleanup = await verifyOwnedProcessCleanupSnapshot(snapshot);
|
||||
state.evidence.ownedProcessIdentityCaptured = true;
|
||||
state.evidence.ownedRunnerObservedCount =
|
||||
snapshot.observedCounts.runner;
|
||||
state.evidence.ownedHelperObservedCount =
|
||||
snapshot.observedCounts.helper;
|
||||
state.evidence.ownedNodeDescendantObservedCount =
|
||||
snapshot.observedCounts.node;
|
||||
state.evidence.ownedBrowserDescendantObservedCount =
|
||||
snapshot.observedCounts.browser;
|
||||
state.evidence.ownedCommandDescendantObservedCount =
|
||||
snapshot.observedCounts.command;
|
||||
state.evidence.ownedRunnerResidualCount =
|
||||
cleanup.residualCounts.runner;
|
||||
state.evidence.ownedHelperResidualCount =
|
||||
cleanup.residualCounts.helper;
|
||||
state.evidence.ownedNodeDescendantResidualCount =
|
||||
cleanup.residualCounts.node;
|
||||
state.evidence.ownedBrowserDescendantResidualCount =
|
||||
cleanup.residualCounts.browser;
|
||||
state.evidence.ownedCommandDescendantResidualCount =
|
||||
cleanup.residualCounts.command;
|
||||
state.evidence.activeCommandChildrenAfterCleanup =
|
||||
cleanup.activeCommandChildCount;
|
||||
state.evidence.activeInteractiveCliSessionsAfterCleanup =
|
||||
cleanup.activeInteractiveCliSessionCount;
|
||||
state.evidence.ownedProcessCleanupPassed = cleanup.clean;
|
||||
if (!cleanup.clean) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-residual-detected',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-verification-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-missing',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSteerRunnerKillSuite() &&
|
||||
state.projectRoot &&
|
||||
@@ -1210,6 +1366,7 @@ if (selfTestRequested) {
|
||||
const safeSummary = {
|
||||
status: state.status,
|
||||
suite: state.suite,
|
||||
providerUsed: false,
|
||||
blocked: state.blocked,
|
||||
cleanup: {
|
||||
performed: state.cleanupPerformed,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import {
|
||||
scopedAgentsSuite,
|
||||
steerRunnerKillSuite,
|
||||
supervisorAutonomousPlayableLaneDefenseSuite,
|
||||
supervisorGameChatSingleMainPlayableSuite,
|
||||
supervisorSwarmAutonomousChatSuite,
|
||||
supervisorSwarmCollaborationPolicyMixedRecoverySuite,
|
||||
supervisorSwarmFinalReplyTransientRetrySuite,
|
||||
@@ -70,6 +71,7 @@ export function parseArguments(args) {
|
||||
suite === supervisorSwarmToolPlanHandoffRunnerKillSuite ||
|
||||
suite === supervisorSwarmAutonomousChatSuite ||
|
||||
suite === supervisorAutonomousPlayableLaneDefenseSuite ||
|
||||
suite === supervisorGameChatSingleMainPlayableSuite ||
|
||||
suite === supervisorSwarmStaticIsolatedAutonomousChatSuite ||
|
||||
suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite ||
|
||||
suite === steerRunnerKillSuite ||
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
} from '../suites/supervisor-swarm.mjs';
|
||||
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
|
||||
export const activeInteractiveCliSessions = new Set();
|
||||
|
||||
export async function prepareCliBinary() {
|
||||
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
|
||||
await runProcess(
|
||||
@@ -154,36 +156,70 @@ export function startInteractiveCli(args) {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
return createInteractiveCliSession(child);
|
||||
}
|
||||
|
||||
export function createInteractiveCliSession(child) {
|
||||
activeCommandChildren.add(child);
|
||||
const session = {
|
||||
child,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
exited: false,
|
||||
exitInfo: null,
|
||||
exitPromise: null,
|
||||
closed: false,
|
||||
closeInfo: null,
|
||||
closePromise: null,
|
||||
stdioClosed: false,
|
||||
stdioCloseInfo: null,
|
||||
spawnError: null,
|
||||
stdinError: null,
|
||||
};
|
||||
activeInteractiveCliSessions.add(session);
|
||||
session.exitPromise = new Promise((resolve) => {
|
||||
const settle = (result) => {
|
||||
if (session.exited) return;
|
||||
activeCommandChildren.delete(child);
|
||||
session.exited = true;
|
||||
session.closed = true;
|
||||
session.exitInfo = result;
|
||||
session.closeInfo = result;
|
||||
resolve(result);
|
||||
};
|
||||
child.once('error', (error) => {
|
||||
session.spawnError = error;
|
||||
settle({ code: null, signal: null, error });
|
||||
});
|
||||
child.once('exit', (code, signal) => {
|
||||
settle({ code, signal, error: null });
|
||||
});
|
||||
});
|
||||
session.closePromise = new Promise((resolve) => {
|
||||
child.on('error', (error) => {
|
||||
child.once('close', (code, signal) => {
|
||||
activeCommandChildren.delete(child);
|
||||
session.closed = true;
|
||||
session.closeInfo = { code: null, signal: null, error };
|
||||
resolve(session.closeInfo);
|
||||
});
|
||||
child.on('close', (code, signal) => {
|
||||
activeCommandChildren.delete(child);
|
||||
session.closed = true;
|
||||
session.closeInfo = { code, signal, error: null };
|
||||
resolve(session.closeInfo);
|
||||
activeInteractiveCliSessions.delete(session);
|
||||
session.stdioClosed = true;
|
||||
session.stdioCloseInfo = {
|
||||
code,
|
||||
signal,
|
||||
error: session.spawnError,
|
||||
};
|
||||
resolve(session.stdioCloseInfo);
|
||||
});
|
||||
});
|
||||
child.stdin?.on('error', (error) => {
|
||||
session.stdinError ??= error;
|
||||
});
|
||||
child.stdout.on('data', (chunk) => {
|
||||
state.transcriptScanner?.scan('interactive-stdout', chunk);
|
||||
state.projectPathTranscriptScanner?.scan('interactive-stdout', chunk);
|
||||
state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk);
|
||||
session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit);
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
state.transcriptScanner?.scan('interactive-stderr', chunk);
|
||||
state.projectPathTranscriptScanner?.scan('interactive-stderr', chunk);
|
||||
state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk);
|
||||
session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit);
|
||||
});
|
||||
@@ -205,16 +241,23 @@ export async function waitForInteractiveCliOutput(
|
||||
predicate,
|
||||
code,
|
||||
timeoutMs,
|
||||
{ allowAfterProcessExit = false } = {},
|
||||
) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const output = interactiveCliOutput(session);
|
||||
if (predicate(output)) return output;
|
||||
if (session.closed) {
|
||||
if (session.exited && !allowAfterProcessExit) {
|
||||
if (session === state.supervisorSwarmCliSession) {
|
||||
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
|
||||
}
|
||||
throw codedError(`${code}-cli-closed`);
|
||||
throw codedError(`${code}-cli-exited`);
|
||||
}
|
||||
if (session.stdioClosed) {
|
||||
if (session === state.supervisorSwarmCliSession) {
|
||||
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
|
||||
}
|
||||
throw codedError(`${code}-cli-stdio-closed`);
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
@@ -223,7 +266,7 @@ export async function waitForInteractiveCliOutput(
|
||||
|
||||
export async function waitForInteractiveCliExit(session, timeoutMs) {
|
||||
const result = await Promise.race([
|
||||
session.closePromise,
|
||||
session.exitPromise,
|
||||
sleep(timeoutMs).then(() => null),
|
||||
]);
|
||||
if (!result) throw codedError('interactive-cli-exit-timeout');
|
||||
@@ -236,26 +279,52 @@ export async function waitForInteractiveCliExit(session, timeoutMs) {
|
||||
}
|
||||
|
||||
export async function closeInteractiveCli(session) {
|
||||
if (!session || session.closed) return;
|
||||
if (session.child.stdin.writable) {
|
||||
if (!session) return null;
|
||||
if (session.exited) return session.exitInfo;
|
||||
if (
|
||||
session.child.stdin.writable &&
|
||||
!session.child.stdin.writableEnded &&
|
||||
!session.child.stdin.destroyed
|
||||
) {
|
||||
session.child.stdin.write('/quit\n');
|
||||
}
|
||||
let result = await Promise.race([
|
||||
session.closePromise,
|
||||
session.exitPromise,
|
||||
sleep(3_000).then(() => null),
|
||||
]);
|
||||
if (!result && !session.closed) {
|
||||
if (!result && !session.exited) {
|
||||
session.child.kill('SIGTERM');
|
||||
result = await Promise.race([
|
||||
session.closePromise,
|
||||
session.exitPromise,
|
||||
sleep(2_000).then(() => null),
|
||||
]);
|
||||
}
|
||||
if (!result && !session.closed) {
|
||||
if (!result && !session.exited) {
|
||||
session.child.kill('SIGKILL');
|
||||
result = await session.closePromise;
|
||||
result = await Promise.race([
|
||||
session.exitPromise,
|
||||
sleep(5_000).then(() => null),
|
||||
]);
|
||||
}
|
||||
assert(Boolean(result), 'interactive-cli-cleanup-timeout');
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function waitForInteractiveCliStdioClose(session, timeoutMs) {
|
||||
if (!session || session.stdioClosed) return session?.stdioCloseInfo ?? null;
|
||||
const result = await Promise.race([
|
||||
session.closePromise,
|
||||
sleep(timeoutMs).then(() => null),
|
||||
]);
|
||||
if (!result) throw codedError('interactive-cli-stdio-close-timeout');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function destroyInteractiveCliOutputStreams(session) {
|
||||
if (!session) return;
|
||||
for (const stream of [session.child.stdout, session.child.stderr]) {
|
||||
if (stream && !stream.destroyed) stream.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runProcess(
|
||||
@@ -329,6 +398,214 @@ export async function runProcess(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSystemProcessIdentities() {
|
||||
if (process.platform === 'win32') {
|
||||
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT;
|
||||
assert(
|
||||
typeof systemRoot === 'string' && path.isAbsolute(systemRoot),
|
||||
'owned-process-snapshot-system-root-invalid',
|
||||
);
|
||||
const powershell = path.join(
|
||||
systemRoot,
|
||||
'System32/WindowsPowerShell/v1.0/powershell.exe',
|
||||
);
|
||||
const metadata = await fs.lstat(powershell);
|
||||
assert(
|
||||
metadata.isFile() && !metadata.isSymbolicLink(),
|
||||
'owned-process-snapshot-powershell-invalid',
|
||||
);
|
||||
const result = await runProcess(
|
||||
powershell,
|
||||
[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate,Name); $processes | ConvertTo-Json -Compress',
|
||||
],
|
||||
{
|
||||
cwd: appRoot,
|
||||
timeoutMs: 30_000,
|
||||
env: { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' },
|
||||
},
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
return (Array.isArray(parsed) ? parsed : [parsed])
|
||||
.map((record) => ({
|
||||
pid: Number(record?.ProcessId),
|
||||
parentPid: Number(record?.ParentProcessId),
|
||||
startedAt: String(record?.CreationDate ?? ''),
|
||||
name: String(record?.Name ?? ''),
|
||||
}))
|
||||
.filter(validSystemProcessIdentity);
|
||||
}
|
||||
assert(
|
||||
process.platform === 'linux' || process.platform === 'darwin',
|
||||
'owned-process-snapshot-platform-unsupported',
|
||||
);
|
||||
const result = await runProcess(
|
||||
'ps',
|
||||
['-A', '-o', 'pid=', '-o', 'ppid=', '-o', 'lstart=', '-o', 'comm='],
|
||||
{ cwd: appRoot, timeoutMs: 30_000 },
|
||||
);
|
||||
return result.stdout
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const fields = line.split(/\s+/u);
|
||||
return {
|
||||
pid: Number(fields[0]),
|
||||
parentPid: Number(fields[1]),
|
||||
startedAt: fields.slice(2, 7).join(' '),
|
||||
name: fields.slice(7).join(' '),
|
||||
};
|
||||
})
|
||||
.filter(validSystemProcessIdentity);
|
||||
}
|
||||
|
||||
function validSystemProcessIdentity(record) {
|
||||
return (
|
||||
Number.isSafeInteger(record?.pid) &&
|
||||
record.pid > 0 &&
|
||||
Number.isSafeInteger(record.parentPid) &&
|
||||
record.parentPid >= 0 &&
|
||||
typeof record.startedAt === 'string' &&
|
||||
record.startedAt.length > 0 &&
|
||||
typeof record.name === 'string' &&
|
||||
record.name.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOwnedProcessCleanupSnapshot(
|
||||
processRecords,
|
||||
{ rootPids = [], runnerPid = null, helperPids = [] } = {},
|
||||
) {
|
||||
assert(
|
||||
Array.isArray(processRecords) &&
|
||||
Array.isArray(rootPids) &&
|
||||
Array.isArray(helperPids),
|
||||
'owned-process-snapshot-input-invalid',
|
||||
);
|
||||
const records = processRecords.filter(validSystemProcessIdentity);
|
||||
const byPid = new Map(records.map((record) => [record.pid, record]));
|
||||
const childrenByParent = new Map();
|
||||
for (const record of records) {
|
||||
const children = childrenByParent.get(record.parentPid) ?? [];
|
||||
children.push(record.pid);
|
||||
childrenByParent.set(record.parentPid, children);
|
||||
}
|
||||
const normalizedRunnerPid = Number.isSafeInteger(runnerPid)
|
||||
? runnerPid
|
||||
: null;
|
||||
const helperPidSet = new Set(
|
||||
helperPids.filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
||||
);
|
||||
const roots = [
|
||||
...new Set(
|
||||
[...rootPids, normalizedRunnerPid, ...helperPidSet].filter(
|
||||
(pid) => Number.isSafeInteger(pid) && pid > 0,
|
||||
),
|
||||
),
|
||||
];
|
||||
assert(roots.length > 0, 'owned-process-snapshot-root-missing');
|
||||
const ownedPids = new Set();
|
||||
const queue = [...roots];
|
||||
while (queue.length > 0) {
|
||||
const pid = queue.shift();
|
||||
if (ownedPids.has(pid)) continue;
|
||||
ownedPids.add(pid);
|
||||
queue.push(...(childrenByParent.get(pid) ?? []));
|
||||
}
|
||||
const identities = [...ownedPids]
|
||||
.map((pid) => byPid.get(pid))
|
||||
.filter(Boolean)
|
||||
.map((record) => ({
|
||||
pid: record.pid,
|
||||
startedAt: record.startedAt,
|
||||
name: record.name,
|
||||
kind: ownedProcessKind(record, normalizedRunnerPid, helperPidSet),
|
||||
}))
|
||||
.sort((left, right) => left.pid - right.pid);
|
||||
return {
|
||||
identities,
|
||||
observedCounts: countOwnedProcessKinds(identities),
|
||||
};
|
||||
}
|
||||
|
||||
function ownedProcessKind(record, runnerPid, helperPids) {
|
||||
if (record.pid === runnerPid) return 'runner';
|
||||
if (helperPids.has(record.pid)) return 'helper';
|
||||
const name = path.basename(record.name).toLowerCase();
|
||||
if (/^node(?:\.exe)?$/u.test(name)) return 'node';
|
||||
if (/^(?:chrome|chromium|msedge|google-chrome)(?:\.exe)?$/u.test(name)) {
|
||||
return 'browser';
|
||||
}
|
||||
return 'command';
|
||||
}
|
||||
|
||||
function countOwnedProcessKinds(identities) {
|
||||
const counts = {
|
||||
runner: 0,
|
||||
helper: 0,
|
||||
node: 0,
|
||||
browser: 0,
|
||||
command: 0,
|
||||
total: identities.length,
|
||||
};
|
||||
for (const identity of identities) counts[identity.kind] += 1;
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function inspectOwnedProcessCleanupResiduals(
|
||||
snapshot,
|
||||
processRecords,
|
||||
{ activeCommandChildCount = 0, activeInteractiveCliSessionCount = 0 } = {},
|
||||
) {
|
||||
assert(
|
||||
Array.isArray(snapshot?.identities) && Array.isArray(processRecords),
|
||||
'owned-process-residual-input-invalid',
|
||||
);
|
||||
const currentByPid = new Map(
|
||||
processRecords
|
||||
.filter(validSystemProcessIdentity)
|
||||
.map((record) => [record.pid, record]),
|
||||
);
|
||||
const residualIdentities = snapshot.identities.filter((identity) => {
|
||||
const current = currentByPid.get(identity.pid);
|
||||
return (
|
||||
current?.startedAt === identity.startedAt &&
|
||||
current?.name === identity.name
|
||||
);
|
||||
});
|
||||
return {
|
||||
residualCounts: countOwnedProcessKinds(residualIdentities),
|
||||
activeCommandChildCount,
|
||||
activeInteractiveCliSessionCount,
|
||||
clean:
|
||||
residualIdentities.length === 0 &&
|
||||
activeCommandChildCount === 0 &&
|
||||
activeInteractiveCliSessionCount === 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function captureOwnedProcessCleanupSnapshot(options) {
|
||||
return buildOwnedProcessCleanupSnapshot(
|
||||
await listSystemProcessIdentities(),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyOwnedProcessCleanupSnapshot(snapshot) {
|
||||
return inspectOwnedProcessCleanupResiduals(
|
||||
snapshot,
|
||||
await listSystemProcessIdentities(),
|
||||
{
|
||||
activeCommandChildCount: activeCommandChildren.size,
|
||||
activeInteractiveCliSessionCount: activeInteractiveCliSessions.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function appendBounded(current, chunk, limit) {
|
||||
const combined = Buffer.concat([current, chunk]);
|
||||
return combined.length <= limit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assert } from '../assertions/core.mjs';
|
||||
import { assert, hashValue } from '../assertions/core.mjs';
|
||||
import { disposableProjectPathVariants } from '../assertions/runtime.mjs';
|
||||
import { fs, os, path, randomUUID } from '../dependencies.mjs';
|
||||
import {
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
isGoalRuntimeSuite,
|
||||
} from '../suites/goal.mjs';
|
||||
import { isResponseStreamSuite } from '../suites/response-stream.mjs';
|
||||
import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs';
|
||||
import {
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite,
|
||||
isSupervisorGameChatSingleMainPlayableSuite,
|
||||
} from '../suites/supervisor-autonomous-playable.mjs';
|
||||
import {
|
||||
isSupervisorSwarmSuite,
|
||||
supervisorSwarmVerificationFixtureSource,
|
||||
@@ -40,27 +43,97 @@ import { effectiveAgentLlmConfig } from './config.mjs';
|
||||
import { runProcess } from './process.mjs';
|
||||
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
|
||||
export async function checkPrerequisites(config) {
|
||||
const requiredAgents = isUserInputRuntimeSuite()
|
||||
export function requiredAgentIdsForSuite() {
|
||||
return isUserInputRuntimeSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
supervisorSwarmDesignAgentId,
|
||||
supervisorSwarmQualityAgentId,
|
||||
]
|
||||
: isIsolatedRunnerSuite()
|
||||
? [mainAgentId]
|
||||
: [mainAgentId, 'quality-review'];
|
||||
const llmConfigured = requiredAgents.every((agentId) => {
|
||||
: isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
mainAgentId,
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
supervisorSwarmDesignAgentId,
|
||||
supervisorSwarmQualityAgentId,
|
||||
]
|
||||
: isIsolatedRunnerSuite()
|
||||
? [mainAgentId]
|
||||
: [mainAgentId, 'quality-review'];
|
||||
}
|
||||
|
||||
export function expectedProviderBindingForSuite(config) {
|
||||
if (
|
||||
isSupervisorGameChatSingleMainPlayableSuite() &&
|
||||
config.agentMode !== 'provider'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const agentIds = requiredAgentIdsForSuite();
|
||||
const effectiveConfigs = agentIds.map((agentId) =>
|
||||
effectiveAgentLlmConfig(config, agentId),
|
||||
);
|
||||
if (
|
||||
effectiveConfigs.length === 0 ||
|
||||
effectiveConfigs.some((effective) =>
|
||||
['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].some(
|
||||
(key) =>
|
||||
typeof effective[key] !== 'string' ||
|
||||
effective[key].trim().length === 0,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const [expected] = effectiveConfigs;
|
||||
const expectedIdentity = [
|
||||
expected.model.trim(),
|
||||
expected.apiKind.trim(),
|
||||
expected.reasoningEffort.trim(),
|
||||
expected.baseUrl.trim(),
|
||||
];
|
||||
if (
|
||||
effectiveConfigs.some(
|
||||
(effective) =>
|
||||
JSON.stringify([
|
||||
effective.model.trim(),
|
||||
effective.apiKind.trim(),
|
||||
effective.reasoningEffort.trim(),
|
||||
effective.baseUrl.trim(),
|
||||
]) !== JSON.stringify(expectedIdentity),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...(isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? { providerAgentMode: 'provider' }
|
||||
: {}),
|
||||
providerModel: expectedIdentity[0],
|
||||
providerApiKind: expectedIdentity[1],
|
||||
providerReasoningEffort: expectedIdentity[2],
|
||||
providerBaseUrlSha256: hashValue(expectedIdentity[3]),
|
||||
boundAgentIds: [...agentIds].sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkPrerequisites(config) {
|
||||
const requiredAgents = requiredAgentIdsForSuite();
|
||||
let llmConfigured = requiredAgents.every((agentId) => {
|
||||
const effective = effectiveAgentLlmConfig(config, agentId);
|
||||
return ['apiKey', 'baseUrl', 'model'].every(
|
||||
(key) =>
|
||||
typeof effective[key] === 'string' && effective[key].trim().length > 0,
|
||||
);
|
||||
});
|
||||
const providerBinding = expectedProviderBindingForSuite(config);
|
||||
if (isSupervisorGameChatSingleMainPlayableSuite()) {
|
||||
llmConfigured = llmConfigured && providerBinding !== null;
|
||||
}
|
||||
const editorApiConfigured = ['apiKey', 'baseUrl'].every(
|
||||
(key) =>
|
||||
typeof config.editorApi?.[key] === 'string' &&
|
||||
@@ -68,6 +141,7 @@ export async function checkPrerequisites(config) {
|
||||
);
|
||||
return {
|
||||
llmConfigured,
|
||||
providerBinding,
|
||||
chromeAvailable:
|
||||
!isIsolatedRunnerSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
@@ -150,7 +224,9 @@ export function supportedBrowserCandidates(platform, environment) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export async function seedDisposableProject() {
|
||||
export async function seedDisposableProject({
|
||||
preserveProductionInitBaseline = false,
|
||||
} = {}) {
|
||||
const prefix = path.join(os.tmpdir(), 'genarrative-agent-runtime-real-e2e-');
|
||||
const sentinelToken = randomUUID();
|
||||
state.projectRoot = await createSentinelOwnedTempDirectory({
|
||||
@@ -189,37 +265,42 @@ export async function seedDisposableProject() {
|
||||
...(isResponseStreamSuite() ? [responseStreamThinkingCanary] : []),
|
||||
];
|
||||
|
||||
const generatedBaselineWrites = preserveProductionInitBaseline
|
||||
? []
|
||||
: [
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
isSupervisorSwarmSuite()
|
||||
? supervisorSwarmVerificationFixtureSource()
|
||||
: isGoalRuntimeSuite() ||
|
||||
isResponseStreamSuite() ||
|
||||
isWebSearchSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? goalRevisionOneVerificationFixtureSource()
|
||||
: goalRevisionTwoVerificationFixtureSource(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
seededGameHtml(),
|
||||
),
|
||||
];
|
||||
await Promise.all([
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
isSupervisorSwarmSuite()
|
||||
? supervisorSwarmVerificationFixtureSource()
|
||||
: isGoalRuntimeSuite() ||
|
||||
isResponseStreamSuite() ||
|
||||
isWebSearchSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? goalRevisionOneVerificationFixtureSource()
|
||||
: goalRevisionTwoVerificationFixtureSource(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
seededGameHtml(),
|
||||
),
|
||||
...generatedBaselineWrites,
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'AGENTS.md'),
|
||||
'# Real E2E Repository Context\n\n- REPOSITORY_CONTEXT_MARKER: preserve the visible text and canvas.\n- Read package.json before verification.\n- Never read or expose .env, game-creator.config.json, or .agent/private-secret.txt.\n',
|
||||
@@ -264,7 +345,16 @@ export async function seedDisposableProject() {
|
||||
mode: 0o600,
|
||||
},
|
||||
);
|
||||
await initializeDisposableGitRepository();
|
||||
await initializeDisposableGitRepository(
|
||||
preserveProductionInitBaseline
|
||||
? [
|
||||
'AGENTS.md',
|
||||
'e2e/isolated-a/evidence.txt',
|
||||
'e2e/isolated-b/evidence.txt',
|
||||
'e2e/isolated-c/evidence.txt',
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function initializeDisposableGitRepository(
|
||||
@@ -310,12 +400,17 @@ export function seededGameHtml() {
|
||||
<main>
|
||||
<h1>${visibleText}</h1>
|
||||
<p id="patch-state">REAL_E2E_TARGET:before</p>
|
||||
<p id="objective">Objective: survive until victory. On defeat, use Restart.</p>
|
||||
<canvas id="game" width="640" height="360"></canvas>
|
||||
</main>
|
||||
<script>
|
||||
const canvas = document.getElementById('game');
|
||||
const context = canvas.getContext('2d');
|
||||
let frame = 0;
|
||||
canvas.addEventListener('pointerdown', () => {
|
||||
frame = 0;
|
||||
document.body.dataset.input = 'pointer';
|
||||
});
|
||||
function draw() {
|
||||
frame += 1;
|
||||
context.fillStyle = '#13293d'; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -332,6 +427,23 @@ export function seededGameHtml() {
|
||||
`;
|
||||
}
|
||||
|
||||
export function productionDefaultGameIndexHtml() {
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Genarrative Game Draft</title>
|
||||
<style>
|
||||
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
|
||||
main { width: min(720px, calc(100vw - 32px)); }
|
||||
</style>
|
||||
</head>
|
||||
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildTaskPrompt(suite) {
|
||||
const editorAssetOutcome =
|
||||
suite === 'full'
|
||||
|
||||
@@ -47,12 +47,45 @@ export async function removeDisposableProject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function providerUsedFromEvidence(evidence) {
|
||||
const requestIdentityCount = evidence?.providerRequestIdentityCount;
|
||||
const startedCount = evidence?.providerLifecycleStartedCount;
|
||||
const terminalCount = evidence?.providerLifecycleTerminalCount;
|
||||
const completedCount = evidence?.providerLifecycleCompletedCount;
|
||||
return (
|
||||
evidence?.evidenceCompleteness === 'complete' &&
|
||||
evidence.providerAgentMode === 'provider' &&
|
||||
evidence.providerBindingMatched === true &&
|
||||
typeof evidence.providerModel === 'string' &&
|
||||
evidence.providerModel.trim().length > 0 &&
|
||||
typeof evidence.providerApiKind === 'string' &&
|
||||
evidence.providerApiKind.trim().length > 0 &&
|
||||
typeof evidence.providerReasoningEffort === 'string' &&
|
||||
evidence.providerReasoningEffort.trim().length > 0 &&
|
||||
/^[0-9a-f]{64}$/u.test(evidence.providerBaseUrlSha256 ?? '') &&
|
||||
Number.isSafeInteger(evidence.providerBoundAgentCount) &&
|
||||
evidence.providerBoundAgentCount > 0 &&
|
||||
Number.isSafeInteger(requestIdentityCount) &&
|
||||
Number.isSafeInteger(startedCount) &&
|
||||
Number.isSafeInteger(terminalCount) &&
|
||||
Number.isSafeInteger(completedCount) &&
|
||||
requestIdentityCount > 0 &&
|
||||
startedCount === requestIdentityCount &&
|
||||
terminalCount === requestIdentityCount &&
|
||||
completedCount === requestIdentityCount &&
|
||||
evidence.providerLifecycleFailedCount === 0 &&
|
||||
evidence.openProviderLifecycleCount === 0 &&
|
||||
evidence.duplicateProviderLifecycleCount === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function buildSummary() {
|
||||
const secretLeakCount =
|
||||
state.transcriptLeakCount + state.projectLeakCount + state.reportLeakCount;
|
||||
const base = {
|
||||
status: state.status,
|
||||
suite: state.suite,
|
||||
providerUsed: providerUsedFromEvidence(state.evidence),
|
||||
config: state.config,
|
||||
blocked: state.blocked,
|
||||
run: {
|
||||
|
||||
@@ -3118,17 +3118,29 @@ export async function countLureLeaks() {
|
||||
if (excluded.has(relative)) continue;
|
||||
const metadata = await fs.lstat(file);
|
||||
if (!metadata.isFile() || metadata.isSymbolicLink()) continue;
|
||||
if (isEmptyExecutionOwnerLock(state.projectRoot, file, metadata)) continue;
|
||||
const content = await fs.readFile(file);
|
||||
count += countExactSecrets(content, state.lures);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function isEmptyExecutionOwnerLock(root, file, metadata) {
|
||||
return (
|
||||
metadata?.isFile?.() === true &&
|
||||
metadata.isSymbolicLink() === false &&
|
||||
metadata.size === 0 &&
|
||||
path.resolve(file) ===
|
||||
path.resolve(root, '.agent/runtime/execution-owner.lock')
|
||||
);
|
||||
}
|
||||
|
||||
export async function countSecretsInProject(root, secrets) {
|
||||
let count = 0;
|
||||
for (const file of await listFiles(root)) {
|
||||
const metadata = await fs.lstat(file);
|
||||
if (!metadata.isFile() || metadata.isSymbolicLink()) continue;
|
||||
if (isEmptyExecutionOwnerLock(root, file, metadata)) continue;
|
||||
count += await countSecretsInFile(file, secrets);
|
||||
}
|
||||
return count;
|
||||
|
||||
@@ -227,6 +227,9 @@ export const supervisorSwarmAutonomousChatSuite =
|
||||
export const supervisorAutonomousPlayableLaneDefenseSuite =
|
||||
'supervisor-autonomous-playable-lane-defense';
|
||||
|
||||
export const supervisorGameChatSingleMainPlayableSuite =
|
||||
'supervisor-game-chat-single-main-playable';
|
||||
|
||||
export const supervisorSwarmStaticIsolatedAutonomousChatSuite =
|
||||
'supervisor-swarm-static-isolated-autonomous-chat';
|
||||
|
||||
@@ -843,15 +846,94 @@ finally:
|
||||
os.close(pidfd)
|
||||
`;
|
||||
|
||||
export const windowsProcessHandleHelperSource = String.raw`
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$nativeSource = @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class GenarrativeOwnedProcessHandle
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr OpenProcess(
|
||||
uint desiredAccess,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool inheritHandle,
|
||||
uint processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool TerminateProcess(IntPtr process, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern uint GetProcessId(IntPtr process);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CloseHandle(IntPtr handle);
|
||||
}
|
||||
'@
|
||||
|
||||
[void](Add-Type -TypeDefinition $nativeSource -ErrorAction Stop)
|
||||
$targetPidText = [Environment]::GetEnvironmentVariable(
|
||||
'AGC_OWNED_RUNNER_PID',
|
||||
[EnvironmentVariableTarget]::Process)
|
||||
if ([String]::IsNullOrWhiteSpace($targetPidText)) { exit 70 }
|
||||
$targetPid = [UInt32]::Parse($targetPidText)
|
||||
$desiredAccess = [UInt32](0x0001 -bor 0x00100000 -bor 0x1000)
|
||||
$handle = [GenarrativeOwnedProcessHandle]::OpenProcess(
|
||||
$desiredAccess,
|
||||
$false,
|
||||
$targetPid)
|
||||
if ($handle -eq [IntPtr]::Zero) { exit 71 }
|
||||
|
||||
try {
|
||||
if ([GenarrativeOwnedProcessHandle]::GetProcessId($handle) -ne $targetPid) {
|
||||
exit 72
|
||||
}
|
||||
[Console]::Out.WriteLine('HANDLE_READY')
|
||||
[Console]::Out.Flush()
|
||||
$command = [Console]::In.ReadLine()
|
||||
if ($command -eq 'CLOSE') { exit 0 }
|
||||
if ($command -ne 'KILL') { exit 73 }
|
||||
if (-not [GenarrativeOwnedProcessHandle]::TerminateProcess($handle, 137)) {
|
||||
exit 74
|
||||
}
|
||||
if ([GenarrativeOwnedProcessHandle]::WaitForSingleObject($handle, 10000) -ne 0) {
|
||||
exit 75
|
||||
}
|
||||
[Console]::Out.WriteLine('HANDLE_EXITED')
|
||||
[Console]::Out.Flush()
|
||||
} finally {
|
||||
[void][GenarrativeOwnedProcessHandle]::CloseHandle($handle)
|
||||
}
|
||||
`;
|
||||
|
||||
export const activeCommandChildren = new Set();
|
||||
|
||||
export const shutdownWaiters = new Set();
|
||||
|
||||
export class StreamingSecretScanner {
|
||||
constructor(secrets) {
|
||||
this.secrets = secrets.map((value) => Buffer.from(value));
|
||||
this.secrets = [];
|
||||
this.secretKeys = new Set();
|
||||
this.tails = new Map();
|
||||
this.count = 0;
|
||||
this.addSecrets(secrets);
|
||||
}
|
||||
|
||||
addSecrets(secrets) {
|
||||
for (const value of secrets) {
|
||||
const secret = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
||||
if (secret.length === 0) continue;
|
||||
const key = secret.toString('base64');
|
||||
if (this.secretKeys.has(key)) continue;
|
||||
this.secretKeys.add(key);
|
||||
this.secrets.push(secret);
|
||||
}
|
||||
}
|
||||
|
||||
scan(source, chunk) {
|
||||
@@ -912,6 +994,7 @@ export const isolatedRunnerState = {
|
||||
export const state = {
|
||||
shutdownSignal: null,
|
||||
linuxPidfdPythonPath: null,
|
||||
windowsProcessHandlePowerShellPath: null,
|
||||
cleanupInProgress: false,
|
||||
userInputCliSession: null,
|
||||
supervisorSwarmCliSession: null,
|
||||
@@ -931,7 +1014,7 @@ export const state = {
|
||||
transcriptLeakCount: 0,
|
||||
projectLeakCount: 0,
|
||||
reportLeakCount: 0,
|
||||
lureLeakCount: 0,
|
||||
lureLeakCount: null,
|
||||
commandOutputMarkerSeenInContext: false,
|
||||
commandOutputContextPages: new Set(),
|
||||
commandMarkerReportLeakCount: 0,
|
||||
@@ -1081,7 +1164,10 @@ export const state = {
|
||||
reportLeakCount: 0,
|
||||
},
|
||||
supervisorAutonomousPlayable: {
|
||||
freshInitBaselineUsed: false,
|
||||
initialGameIndexSha256: null,
|
||||
expectedProviderBinding: null,
|
||||
effectiveProviderBinding: null,
|
||||
stdinWriteCount: 0,
|
||||
stdinEnded: false,
|
||||
stdinBytes: 0,
|
||||
@@ -1089,6 +1175,7 @@ export const state = {
|
||||
cliOutput: '',
|
||||
privateValues: [],
|
||||
reportLeakCount: 0,
|
||||
ownedProcessCleanupSnapshot: null,
|
||||
},
|
||||
supervisorSwarm: {
|
||||
effectiveModel: null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user