openyida 2026.10.14 → 2026.10.15-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +10 -20
  2. package/bin/yida.js +5 -0
  3. package/lib/agent/cmd.js +1 -1
  4. package/lib/agent/frozen-bundle.js +25 -15
  5. package/lib/agent/private-directory.js +47 -20
  6. package/lib/agent/provider-preflight.js +13 -10
  7. package/lib/agent/runtime-catalog.json +15 -12
  8. package/lib/agent/runtime-package.js +7 -3
  9. package/lib/agent/stdio.js +16 -5
  10. package/lib/app/app-entry.js +16 -3
  11. package/lib/app/update-form-config.js +5 -2
  12. package/lib/core/agent-capabilities.js +1 -0
  13. package/lib/core/command-manifest.js +61 -13
  14. package/lib/core/locales/en.js +21 -1
  15. package/lib/core/locales/zh.js +21 -1
  16. package/lib/core/theme-widget.js +14 -1
  17. package/lib/design-plan/confirmation.js +23 -2
  18. package/lib/design-plan/design-plan.js +3 -3
  19. package/lib/design-plan/entry-navigation.js +3 -1
  20. package/lib/design-plan/init.js +2 -1
  21. package/lib/design-plan/materialize.js +1 -1
  22. package/lib/design-plan/patch.js +10 -3
  23. package/lib/design-plan/visual-policy.js +26 -8
  24. package/lib/style/data-manage-presets.json +146 -0
  25. package/lib/style/data-manage-style.js +141 -0
  26. package/lib/style/native-data-manage.js +300 -0
  27. package/package.json +7 -6
  28. package/yida-skills/SKILL.md +2 -1
  29. package/yida-skills/references/field-and-url-reference.md +9 -8
  30. package/yida-skills/references/local-agent-preflight.md +5 -2
  31. package/yida-skills/skills/yida-app/SKILL.md +7 -1
  32. package/yida-skills/skills/yida-app/references/entry-navigation.md +31 -5
  33. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +13 -25
  34. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +14 -4
  35. package/yida-skills/skills/yida-app/workflow/step-2-design.md +6 -1
  36. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +6 -1
  37. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +14 -6
  38. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +1 -1
  39. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +3 -1
  40. package/yida-skills/skills/yida-data-manage-ui/SKILL.md +70 -0
  41. package/yida-skills/skills/yida-data-manage-ui/references/column-emphasis.md +20 -0
  42. package/yida-skills/skills/yida-data-manage-ui/references/custom-layouts.md +23 -0
  43. package/yida-skills/skills/yida-data-manage-ui/references/style-decisions.md +32 -0
  44. package/yida-skills/skills/yida-data-manage-ui/references/token-and-css.md +77 -0
  45. package/yida-skills/skills/yida-data-management/SKILL.md +2 -0
  46. package/yida-skills/skills/yida-design/SKILL.md +7 -3
  47. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +32 -4
  48. package/yida-skills/skills/yida-design/references/reference-visual-dna.md +34 -0
  49. package/yida-skills/skills/yida-design/references/theme-selection.md +35 -18
  50. package/yida-skills/skills/yida-design/sub_skill/ui-improve/SKILL.md +2 -0
  51. package/yida-skills/skills/yida-design/sub_skill/ui-improve/references/confirmation.md +1 -1
  52. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +5 -3
  53. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +7 -8
  54. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -1
  55. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +2 -0
  56. package/yida-skills/skills/yida-nav-shell/SKILL.md +2 -2
  57. package/yida-skills/skills/yida-page-config/SKILL.md +2 -2
  58. package/yida-skills/skills/yida-publish-page/SKILL.md +1 -1
  59. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +4 -2
  60. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +1 -1
  61. package/yida-skills/skills-index.json +42 -0
package/README.md CHANGED
@@ -47,7 +47,7 @@ Example applications built with the themes below. Click an image to view it at f
47
47
  </tr>
48
48
  </table>
49
49
 
50
- Choose an **application theme or an independent creative direction**. The agent filters styles by your task and navigation needs. Pop art is offered only on an explicit request or for a primarily showcase-oriented task. [Browse more application examples](https://demo.aliwork.com/o/openyida/helpCenter?openyidaRoute=showcase).
50
+ Use an **application theme or an independent creative direction**. Describe a style you like, or provide a reference website or design image to customize your app’s appearance. [Browse more application examples](https://demo.aliwork.com/o/openyida/helpCenter?openyidaRoute=showcase).
51
51
 
52
52
  ## Core Capabilities
53
53
 
@@ -64,7 +64,7 @@ Choose an **application theme or an independent creative direction**. The agent
64
64
  ## From Request to Delivery
65
65
 
66
66
  1. **Describe the goal.** Explain who will use the app, what they need to do, and any existing app or reference material.
67
- 2. **Confirm requirements and style together.** Ready questions are grouped early. Style options use a name and a short explanation; reference images appear only when you ask to see them.
67
+ 2. **Share requirements and preferences.** Add the business details and appearance preferences that matter to you. Include any colors, websites, or design images you like.
68
68
  3. **Choose how to build.** Fast suits clear requirements you want to move forward with. Plan produces a reviewable proposal and waits for approval before creating resources.
69
69
  4. **Review and refine.** Check the delivered app and common actions, then request page, field, or workflow changes. Your chosen style is reused unless you ask to change it.
70
70
 
@@ -72,7 +72,7 @@ For existing-app improvements, review the visual proposal before backups and imp
72
72
 
73
73
  ## Recent Updates
74
74
 
75
- **2026.10.13** adds existing-app visual improvements and theme backup/restore, reopens five refined themes, and improves narrow-screen layouts, readability, and style selection. Optional previews show only titles and example images; selecting a style does not lead to repeated questions. [Read the full changelog](./CHANGELOG.md#20261013---2026-10-13).
75
+ **2026.10.14-1** improves appearance customization, with better support for style descriptions, reference websites, and design images. [Read the full changelog](./CHANGELOG.md#20261014-1---2026-10-14).
76
76
 
77
77
  ## Quick Start
78
78
 
@@ -443,6 +443,9 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
443
443
  | `openyida design-plan materialize <build-plan.json> [--from-preview \| --business-file <json> --visual-file <json>] [--output-dir <dir>] [--rebase-parts] [--check] [--json]` | Generate or update plan documents and theme with consistency checks |
444
444
  | `openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]` | Patch plan fields and optionally sync documents and theme |
445
445
  | `openyida update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light\|dark\|white\|gray] [--logo-source appIcon\|customImage] [--layout side\|top\|l_shape] [--hide-app-nav\|--show-app-nav]` | Update app info |
446
+ | `openyida data-manage-style guide [--json]` | Inspect data-management UI directions, tokens and scoped CSS guidance |
447
+ | `openyida data-manage-style list [--json]` | List optional data-management recipe examples |
448
+ | `openyida data-manage-style generate --app <appType> --form <formUuid> --style <id> --base-css <theme.css> --output <new.css> [--focus balanced\|actions] [--field-id <actualId>]... [--emphasize-field <actualId>]... [--tag-field <selectFieldId>]... [--follow-shell] [--json]` | Generate data-management CSS from an optional recipe (local only) |
446
449
  | `openyida app-theme snapshot <appType> --output-dir <new-dir> [--json]` | Archive application theme settings and CSS (read-only) |
447
450
  | `openyida app-theme restore <appType> --snapshot <before.json> --expected-current <after.json> [--check \| --confirm --output-dir <new-dir>] [--json]` | Check or restore an application theme snapshot (supported exact values only) |
448
451
  | `openyida upgrade-app-theme <appType> --explicit-request [--confirm \| --prepare --output-dir <dir>] [--json]` | Upgrade an AI-built legacy app theme (explicit request and ask_human confirmation required) |
@@ -633,30 +636,17 @@ Form field definitions can include `alias` or `componentAlias` to populate Yida
633
636
 
634
637
  `openyida integration create` supports form events (`insert`, `update`, `delete`, `comment`) and approval events (`processFinish`, `activityTask`; aliases: `approval`, `approvalNode`). Approval events require `--approval-actions agree,disagree,terminated`; `activityTask` also requires `--approval-node-ids <nodeId,...>`. Passing `--process-code` selects a full graph replacement and now requires explicit `--replace`; it is not a safe update. `integration update` currently performs capability detection only: because full platform `processJson` + `viewJson` readback is unproven, it writes a redacted local probe artifact and returns `PLATFORM_PROBE_REQUIRED` before authentication, spec reading, or remote writes. It does not edit a flow.
635
638
 
636
- ### Local Agent development preview
639
+ ### Connect a Local Agent
637
640
 
638
- `openyida agent` is a thin Node launcher for a separately supplied Go runtime. Node does not connect to the agent service or manage provider sessions. Four signed platform packages support macOS/Windows x64 and ARM64; npm installs the package matching the current platform. Node does not discover a Go executable on `PATH` or install provider CLIs.
641
+ Use an installed Qoder or Codex CLI on your computer to run tasks from the Yida web app. Local Agent supports macOS and Windows; the ordinary OpenYida CLI also supports Linux.
639
642
 
640
- The Local Agent Beta production allowlist contains macOS and Windows only. Linux remains supported by the ordinary OpenYida CLI and by the explicit development-runtime workflow below, but Linux Local Agent packages are not published and production `connect`/`run` are rejected until native acceptance is complete.
643
+ Check your local environment, then follow the connection instructions in the Yida web app:
641
644
 
642
645
  ```bash
643
646
  openyida agent doctor --json
644
- openyida agent doctor --development-runtime --runtime-path /absolute/path/openyida-runtime --runtime-manifest /absolute/path/runtime-manifest.json
645
- openyida agent connect --development-runtime --runtime-path /absolute/path/openyida-runtime --runtime-manifest /absolute/path/runtime-manifest.json --endpoint https://agent.example.test --endpoint-id configured-platform-id
646
- openyida agent run --development-runtime --runtime-path /absolute/path/openyida-runtime --runtime-manifest /absolute/path/runtime-manifest.json --endpoint https://agent.example.test --endpoint-id configured-platform-id --provider qoder --provider-path /absolute/path/qodercli
647
647
  ```
648
648
 
649
- Development manifest v1 specifies runtime version, protocol `1`, Node-style OS/architecture names, and `binary: {file, sha256}`; it requires the explicit `--development-runtime` flag. This is an integrity check, **not a publisher signature**. The production loader requires a strictly validated v2 manifest, exact approved platform-package version/digest, and an Ed25519 signature over the original manifest bytes using trust roots bundled in the Node host. The bundled catalog pins the approved runtime version, manifest digest and signing public key. See the [launcher contract](lib/agent/PROTOCOL.md).
650
-
651
- `connect` supports two equivalent enrollment entries. CLI-first opens the Yida browser approval page and returns through a state/PKCE-protected loopback callback. Web-first copies one five-minute command containing `--enroll`; the token is single-use and carries no business authority. Neither path reads or uploads an ordinary OpenYida login profile.
652
-
653
- One installation may connect to multiple organizations. Node keeps only a shared Installation ID at the installation root; each organization authorization gets an isolated private Connection state directory, device credentials, journals, control socket, and frozen bundle. `agent connect` and `agent run` start an independent background daemon by default and return once it is ready. `status`, `logs`, `disconnect`, and `logout` enumerate valid Connections. Background mode is not boot-time autostart. Each Connection registers all locally discovered Qoder and Codex Runtimes, and the web UI selects computer, CLI, and model.
654
-
655
- For each attempt, Go injects `OPENYIDA_MANAGED_RUN=1`, the cloud user/corp/app/run/attempt context, and a fixed-value Tianshu task grant with an initial 60-minute lease. The CLI uses that grant instead of local auth and sends scope-correlation headers. While the authenticated device connection and server-side Run remain active, yida-agent can renew the same bearer server-to-server; the CLI cannot renew it and never receives a task refresh token. Tianshu independently checks the grant against the requested app and rejects application creation, identity/environment changes, cross-application requests and unsupported organization-wide operations; declared application-bound batches are supported. `formUuid` remains optional per-turn page-focus context, so one app-bound session can work across pages in that application. Without `OPENYIDA_MANAGED_RUN`, existing login, token refresh, and command behavior are unchanged.
656
-
657
- Before `connect` or `run`, Node now freezes the verified runtime bytes, CLI, full production dependency closure, and packaged Skills into a private content-addressed bundle. Source changes during copying fail closed. Existing bundles are verified, never silently repaired or garbage-collected by Node, and survive replacement/removal of the original npm directory. Go owns active/session references and version activation; the external Node executable and provider installations are not copied or managed by npm. A separate parent-watch pipe and a 15-second stop grace prevent normal config EOF from being mistaken for parent death; an unconfirmed forced stop remains unknown.
658
-
659
- This is not an OS sandbox or an authorization boundary against arbitrary shell code. Production signing/release infrastructure, full Go/provider recovery and upgrade acceptance, and OS/architecture-specific provider E2E are not claimed by these fake-process tests. Auto-update is disabled for `agent` and managed invocations; ordinary CLI commands neither spawn nor download the runtime.
649
+ After connecting, the service runs in the background. Use `openyida agent status` to check its status, `openyida agent logs` to view logs, and `openyida agent disconnect` to disconnect. After restarting your computer, run `openyida agent run` to reconnect.
660
650
 
661
651
  ## Agent Skills
662
652
 
package/bin/yida.js CHANGED
@@ -942,6 +942,11 @@ async function main() {
942
942
  break;
943
943
  }
944
944
 
945
+ case 'data-manage-style': {
946
+ await require('../lib/style/data-manage-style').run(args);
947
+ break;
948
+ }
949
+
945
950
  case 'app-theme': {
946
951
  await require('../lib/app/app-theme').run(args);
947
952
  break;
package/lib/agent/cmd.js CHANGED
@@ -285,7 +285,7 @@ async function run(argv = [], options = {}) {
285
285
  // Inventory includes signed-out CLIs. Runtime reports their auth status;
286
286
  // the control plane independently enforces task readiness.
287
287
  config.providers = providerCheck.providers.filter(p => p.registrable).map(p => ({
288
- profileId: 'openyida.' + p.provider, provider: p.provider, executable: p.executable,
288
+ profileId: 'openyida.' + p.provider, provider: p.provider, executable: p.executable, ...(p.version ? { version: p.version } : {}),
289
289
  }));
290
290
  }
291
291
  if (managedConnections && args.command === 'connect') {
@@ -3,9 +3,11 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { CliError } = require('../core/cli-error');
6
+ const { randomUUID } = require('crypto');
7
+ const { mkdirPrivate, mkdirPrivateDirectories } = require('./private-directory');
6
8
  const { t } = require('../core/i18n');
7
- const { digest, readRegularFile, platformPackageName } = require('./runtime-package');
8
- const { parseStrictJson, PRODUCTION_RUNTIME_PLATFORMS } = require('./runtime-manifest');
9
+ const { digest, readRegularFile, platformPackageName, canonicalPath } = require('./runtime-package');
10
+ const { parseStrictJson } = require('./runtime-manifest');
9
11
 
10
12
  const MAX_FILE_BYTES = 256 * 1024 * 1024;
11
13
  const MAX_BUNDLE_BYTES = 1024 * 1024 * 1024;
@@ -37,7 +39,7 @@ function privateDirectory(directory) {
37
39
  let ancestor = directory;
38
40
  while (!fs.existsSync(ancestor)) {suffix.unshift(path.basename(ancestor)); ancestor = path.dirname(ancestor);}
39
41
  if (fs.lstatSync(ancestor).isSymbolicLink()) {throw bundleError();}
40
- let current = fs.realpathSync(ancestor);
42
+ let current = canonicalPath(ancestor);
41
43
  const uid = typeof process.getuid === 'function' ? process.getuid() : null;
42
44
  for (let parent = current; ; parent = path.dirname(parent)) {
43
45
  const stat = fs.lstatSync(parent);
@@ -46,6 +48,7 @@ function privateDirectory(directory) {
46
48
  (stat.mode & 0o022) && !(stat.uid === 0 && stat.mode & 0o1000))) {throw bundleError('AGENT_BUNDLE_UNSAFE_DIRECTORY');}
47
49
  if (parent === path.dirname(parent)) {break;}
48
50
  }
51
+ if (process.platform === 'win32' && suffix.length) {mkdirPrivate(path.join(current, ...suffix));}
49
52
  for (const component of suffix) {
50
53
  current = path.join(current, component);
51
54
  try {fs.mkdirSync(current, { mode: 0o700 });} catch (error) {if (error.code !== 'EEXIST') {throw error;}}
@@ -67,7 +70,7 @@ function findDependency(root, name) {
67
70
  // npm subprocess, or an install hook. Preserve each package's own resolution.
68
71
  for (let current = root; ; current = path.dirname(current)) {
69
72
  const candidate = path.join(current, 'node_modules', name);
70
- if (fs.existsSync(path.join(candidate, 'package.json'))) {return fs.realpathSync(candidate);}
73
+ if (fs.existsSync(path.join(candidate, 'package.json'))) {return canonicalPath(candidate);}
71
74
  if (current === path.dirname(current)) {return null;}
72
75
  }
73
76
  }
@@ -126,7 +129,7 @@ function verifyBundle(root, expectedId) {
126
129
  }
127
130
 
128
131
  function materializeBundle(runtime, config, options = {}) {
129
- const sourceRoot = fs.realpathSync(config.node.packageRoot);
132
+ const sourceRoot = canonicalPath(config.node.packageRoot);
130
133
  const cache = privateDirectory(options.bundleCache || path.join(config.stateDir, 'bundles'));
131
134
  let stage;
132
135
  const pendingFiles = [];
@@ -197,10 +200,7 @@ function materializeBundle(runtime, config, options = {}) {
197
200
  try {
198
201
  const metadata = packageMetadata(sourceRoot);
199
202
  if (metadata.name !== 'openyida' || typeof metadata.version !== 'string') {throw bundleError();}
200
- // Explicit development runtimes may run on CI platforms without a public
201
- // runtime package. Keep the production gate and supported-package exclusion.
202
- runtimePackage = runtime.development === true && !PRODUCTION_RUNTIME_PLATFORMS.includes(process.platform)
203
- ? undefined : platformPackageName(process.platform, process.arch);
203
+ runtimePackage = runtime.development === true ? `@openyida/local-agent-runtime-${process.platform}-${process.arch}` : platformPackageName(process.platform, process.arch);
204
204
  // Deliberate installed CLI allowlist: no checkout, private state, user
205
205
  // project, devDependencies, postinstall execution, or global Skills links.
206
206
  for (const directory of ['bin', 'lib', 'yida-skills']) {
@@ -256,13 +256,23 @@ function materializeBundle(runtime, config, options = {}) {
256
256
  const id = digest(raw);
257
257
  const root = path.join(cache, id);
258
258
  if (!fs.existsSync(root)) {
259
- stage = fs.mkdtempSync(path.join(cache, '.staging-'));
259
+ // Windows may otherwise assign the elevated process's Administrators
260
+ // group as owner. Go requires the current user, as for credential state.
261
+ if (process.platform === 'win32') {
262
+ stage = path.join(cache, '.staging-' + randomUUID());
263
+ mkdirPrivate(stage);
264
+ } else {stage = fs.mkdtempSync(path.join(cache, '.staging-'));}
260
265
  fs.chmodSync(stage, 0o700);
266
+ if (process.platform === 'win32') {
267
+ // Inherited DACLs do not bind nested owners under an elevated token.
268
+ // Create every parent privately in one PowerShell invocation before writes.
269
+ mkdirPrivateDirectories(pendingFiles.map(file => path.dirname(path.join(stage, file.relative))));
270
+ }
261
271
  for (const file of pendingFiles) {
262
272
  const bytes = file.source ? readRegularFile(file.source, MAX_FILE_BYTES).bytes : file.bytes;
263
273
  if (digest(bytes) !== file.sha256) {throw bundleError('AGENT_BUNDLE_SOURCE_CHANGED');}
264
274
  const target = path.join(stage, file.relative);
265
- fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
275
+ if (process.platform !== 'win32') {fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });}
266
276
  fs.writeFileSync(target, bytes, { flag: 'wx', mode: file.mode });
267
277
  fs.chmodSync(target, file.mode);
268
278
  }
@@ -275,11 +285,11 @@ function materializeBundle(runtime, config, options = {}) {
275
285
  catch (error) {
276
286
  if (!['EEXIST', 'ENOTEMPTY', 'EACCES', 'EPERM'].includes(error.code) || !fs.existsSync(root)) {throw error;}
277
287
  }
278
- } else {
279
- // Never trust a version or mtime cache. Check source stability and every
280
- // byte of the existing bundle, including unexpected entries, on reuse.
281
- verifySources();
282
288
  }
289
+ // On reuse, source bytes were hashed to compute this exact manifest ID.
290
+ // We execute only the existing bundle, whose complete inventory and bytes
291
+ // are verified below. Reopening the source snapshot cannot make that bundle
292
+ // more trustworthy. New publication still checks source stability above.
283
293
  if (published) {fs.chmodSync(root, 0o500);}
284
294
  verifyBundle(root, id);
285
295
  const bundle = { schemaVersion: 1, id, root, manifestPath: path.join(root, 'bundle-manifest.json'), manifestSha256: id,
@@ -13,26 +13,32 @@ function directoryError() {
13
13
  // Node's mkdir mode is ignored on Windows. Create each missing directory with
14
14
  // an inheritable private DACL before any credentials or logs are written.
15
15
  // Existing directories are never re-permissioned here.
16
- function mkdirPrivate(directory) {
16
+ function mkdirPrivateDirectories(directories) {
17
17
  if (process.platform !== 'win32') {
18
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
18
+ for (const directory of directories) {fs.mkdirSync(directory, { recursive: true, mode: 0o700 });}
19
19
  return;
20
20
  }
21
- const target = path.resolve(directory);
22
- const missing = [];
23
- let ancestor = target;
24
- while (!fs.existsSync(ancestor)) {
25
- missing.unshift(ancestor);
26
- ancestor = path.dirname(ancestor);
27
- }
28
- for (let parent = ancestor; ; parent = path.dirname(parent)) {
29
- const stat = fs.lstatSync(parent);
30
- if (!stat.isDirectory() || stat.isSymbolicLink()) {throw directoryError();}
31
- if (parent === path.dirname(parent)) {break;}
21
+ const targets = [...new Set(directories.map(directory => path.resolve(directory)))];
22
+ const pending = new Set();
23
+ for (const target of targets) {
24
+ const missing = [];
25
+ let ancestor = target;
26
+ while (!fs.existsSync(ancestor)) {
27
+ missing.unshift(ancestor);
28
+ ancestor = path.dirname(ancestor);
29
+ }
30
+ for (let parent = ancestor; ; parent = path.dirname(parent)) {
31
+ const stat = fs.lstatSync(parent);
32
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {throw directoryError();}
33
+ if (parent === path.dirname(parent)) {break;}
34
+ }
35
+ for (const directory of missing) {pending.add(directory);}
32
36
  }
37
+ const missing = [...pending];
33
38
  if (!missing.length) {return;}
34
39
  const script = `
35
40
  $ErrorActionPreference = 'Stop'
41
+ $reader = [System.IO.StreamReader]::new([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
36
42
  $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
37
43
  $acl = New-Object System.Security.AccessControl.DirectorySecurity
38
44
  $acl.SetOwner($identity)
@@ -42,22 +48,43 @@ foreach ($sid in @($identity.Value, 'S-1-5-18', 'S-1-5-32-544')) {
42
48
  $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($principal, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
43
49
  $acl.AddAccessRule($rule)
44
50
  }
45
- foreach ($directory in ($env:OPENYIDA_PRIVATE_DIRECTORIES | ConvertFrom-Json)) {
46
- if ([System.IO.Directory]::Exists($directory)) {throw 'Directory changed during creation'}
47
- [System.IO.Directory]::CreateDirectory($directory, $acl) | Out-Null
51
+ foreach ($directory in ($reader.ReadToEnd() | ConvertFrom-Json)) {
52
+ # Concurrent launchers may create the same parent. Never rewrite a winner;
53
+ # accept it only when its actual owner and grants meet the same private policy.
54
+ # Extended-length paths avoid the .NET Framework MAX_PATH limit. Paths
55
+ # are already absolute and validated by the Node caller.
56
+ $nativePath = $directory
57
+ if (-not $directory.StartsWith('\\\\?\\')) {
58
+ if ($directory.StartsWith('\\\\')) { $nativePath = '\\\\?\\UNC\\' + $directory.Substring(2) }
59
+ else { $nativePath = '\\\\?\\' + $directory }
60
+ }
61
+ [System.IO.Directory]::CreateDirectory($nativePath, $acl) | Out-Null
62
+ $actual = (New-Object System.IO.DirectoryInfo($nativePath)).GetAccessControl()
63
+ if ($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $identity.Value) {throw 'Private directory owner differs'}
64
+ foreach ($rule in $actual.Access) {
65
+ $sid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
66
+ if ($rule.AccessControlType -ne 'Allow' -or $sid -notin @($identity.Value, 'S-1-5-18', 'S-1-5-32-544')) {throw 'Private directory access differs'}
67
+ }
48
68
  }
49
69
  `;
50
70
  try {
51
71
  execFileSync(path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
52
72
  ['-NoProfile', '-NonInteractive', '-Command', script], {
53
73
  windowsHide: true, timeout: 15000, stdio: 'pipe',
54
- env: { ...process.env, OPENYIDA_PRIVATE_DIRECTORIES: JSON.stringify(missing) },
74
+ // Stdin avoids Windows environment-variable size limits for full bundles.
75
+ input: JSON.stringify(missing),
55
76
  });
56
77
  } catch {
57
78
  throw directoryError();
58
79
  }
59
- const stat = fs.lstatSync(target);
60
- if (!stat.isDirectory() || stat.isSymbolicLink()) {throw directoryError();}
80
+ for (const target of targets) {
81
+ const stat = fs.lstatSync(target);
82
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {throw directoryError();}
83
+ }
84
+ }
85
+
86
+ function mkdirPrivate(directory) {
87
+ mkdirPrivateDirectories([directory]);
61
88
  }
62
89
 
63
- module.exports = { mkdirPrivate };
90
+ module.exports = { mkdirPrivate, mkdirPrivateDirectories };
@@ -99,18 +99,14 @@ function discoverProviders(env = process.env, platform = process.platform, home)
99
99
  }));
100
100
  }
101
101
 
102
- function powerShellLiteral(value) {
103
- return `'${String(value).replace(/'/g, "''")}'`;
104
- }
105
-
106
- // Match the Go Runtime's npm launcher handling without cmd.exe interpolation.
102
+ // Use the npm PowerShell sibling without cmd.exe or command-string interpolation.
107
103
  function probeCommand(executable, args, platform = process.platform, env = process.env) {
108
104
  if (platform !== 'win32' || !/\.(cmd|bat)$/i.test(executable)) { return { executable, args }; }
109
105
  const script = executable.replace(/\.(cmd|bat)$/i, '.ps1');
110
106
  if (!exists(script)) { throw new Error('npm launcher has no PowerShell sibling; select a native CLI'); }
111
- const command = `& ${[script, ...args].map(powerShellLiteral).join(' ')}`;
112
107
  return { executable: path.join(env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe'),
113
- args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command] };
108
+ // -File keeps redirected stdin available to ACP and passes arguments as data.
109
+ args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script, ...args] };
114
110
  }
115
111
 
116
112
  // No prompts, model calls, login flows or provider output enter our report.
@@ -190,7 +186,14 @@ async function checkProvider(provider, executable, options) {
190
186
  if (version.status !== 'passed') { return { status: version.status, cliDetected: false }; }
191
187
  // Installation evidence is independent of authentication/protocol readiness.
192
188
  // Apply this consistently to every enabled provider, including future adapters.
193
- const detected = { cliDetected: true };
189
+ // Reuse only a bounded, recognizable version line from this exact probe.
190
+ // Arbitrary provider banners/stderr never become launch metadata.
191
+ const versionLine = String(version.output || '').split(/\r?\n/).map(line => line.trim()).find(line =>
192
+ line.length <= 64 && /^(?:(?:codex(?:-cli)?|qoder(?:cli| cli)?|opencode)\s+)?v?\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)?(?:\s+\([0-9a-z ._-]+\))?$/i.test(line));
193
+ const detected = { cliDetected: true, ...(versionLine ? { version: versionLine } : {}) };
194
+ // Connect/run only establish installation inventory. The runtime discovers live
195
+ // protocol, login and models for EVERY registered provider; doctor probes them here.
196
+ if (options.inventoryOnly) { return { ...detected, status: 'cli_detected', authKnown: false, protocolProbe: 'not_run' }; }
194
197
  if (provider === 'opencode') { return { ...detected, status: 'available', authKnown: false, protocolProbe: 'not_run' }; }
195
198
  const protocol = await execute(executable, provider === 'qoder' ? ['--acp'] : ['app-server', '--listen', 'stdio://'], provider, options);
196
199
  if (protocol.status === 'login_required') {
@@ -234,9 +237,9 @@ async function preflight(args, options = {}) {
234
237
  };
235
238
  for (const candidate of entry.candidates.slice(0, 4)) {
236
239
  executable = candidate;
237
- result = await checkProvider(entry.provider, executable, { ...options, probe: boundedProbe });
240
+ result = await checkProvider(entry.provider, executable, { ...options, inventoryOnly: ['connect', 'run'].includes(args.command), probe: boundedProbe });
238
241
  if (result.cliDetected && !discovered) { discovered = { result, executable }; }
239
- if (Date.now() >= deadline || ['ready', 'available', 'login_required', 'probe_cancelled'].includes(result.status)) { break; }
242
+ if (Date.now() >= deadline || ['ready', 'available', 'cli_detected', 'login_required', 'probe_cancelled'].includes(result.status)) { break; }
240
243
  }
241
244
  if (!result.cliDetected && result.status !== 'probe_cancelled' && discovered) {
242
245
  ({ result, executable } = discovered);
@@ -3,6 +3,9 @@
3
3
  "keys": {
4
4
  "openyida-runtime-public-beta-20260923-1": {
5
5
  "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEApl2Q+64rX997KxEQUY2yU8Xnc7sM3NRQ8Nf0bAi9Hck=\n-----END PUBLIC KEY-----\n"
6
+ },
7
+ "openyida-runtime-public-beta-20261012-1": {
8
+ "publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAKaA5RBJXWVhQa+9ehmR3x7WQCAlVGxbJULG9T17qmRc=\n-----END PUBLIC KEY-----\n"
6
9
  }
7
10
  },
8
11
  "releases": [
@@ -10,33 +13,33 @@
10
13
  "platform": "darwin",
11
14
  "arch": "arm64",
12
15
  "packageName": "@openyida/local-agent-runtime-darwin-arm64",
13
- "packageVersion": "0.1.1",
14
- "runtimeVersion": "0.1.1",
15
- "manifestSha256": "7f3d8e2596210b50db6b664db902f1036a1d51a87fd80e298a50d337d6a44205"
16
+ "packageVersion": "0.1.3",
17
+ "runtimeVersion": "0.1.3",
18
+ "manifestSha256": "1a9f5e148491c350457a41f095de9598775a2a349d872c43877c9fad63c09e28"
16
19
  },
17
20
  {
18
21
  "platform": "darwin",
19
22
  "arch": "x64",
20
23
  "packageName": "@openyida/local-agent-runtime-darwin-x64",
21
- "packageVersion": "0.1.1",
22
- "runtimeVersion": "0.1.1",
23
- "manifestSha256": "e4922eaff8f0000737ede59a7beeb30f88c8a2c97f7a93c871bfe9938d16dc3d"
24
+ "packageVersion": "0.1.3",
25
+ "runtimeVersion": "0.1.3",
26
+ "manifestSha256": "46918d51efd40b73a645487525f2f7bdca94124ffb3ad6cdae6e9641f28b3963"
24
27
  },
25
28
  {
26
29
  "platform": "win32",
27
30
  "arch": "x64",
28
31
  "packageName": "@openyida/local-agent-runtime-win32-x64",
29
- "packageVersion": "0.1.1",
30
- "runtimeVersion": "0.1.1",
31
- "manifestSha256": "3f18fd2f73870e96af8d6eab8edb7594575cf870f90ac9cf1d1506f03f854023"
32
+ "packageVersion": "0.1.3",
33
+ "runtimeVersion": "0.1.3",
34
+ "manifestSha256": "64d471a37c54650e25c214e08d7ca7fa8dde068decfcbe71bd8abcbb2b7e74c6"
32
35
  },
33
36
  {
34
37
  "platform": "win32",
35
38
  "arch": "arm64",
36
39
  "packageName": "@openyida/local-agent-runtime-win32-arm64",
37
- "packageVersion": "0.1.1",
38
- "runtimeVersion": "0.1.1",
39
- "manifestSha256": "0af4afe0bb61466e702500609948b0c3ad24546fe2f80de31cf20ff86ad87266"
40
+ "packageVersion": "0.1.3",
41
+ "runtimeVersion": "0.1.3",
42
+ "manifestSha256": "7b1ff527dc54285084eac0773d273fdfb892ce5ca352a810767c64eefc8e59c0"
40
43
  }
41
44
  ]
42
45
  }
@@ -11,6 +11,10 @@ const bundledCatalog = require('./runtime-catalog.json');
11
11
 
12
12
  const PROTOCOL_VERSION = 1;
13
13
 
14
+ function canonicalPath(value) {
15
+ return process.platform === 'win32' ? fs.realpathSync.native(value) : fs.realpathSync(value);
16
+ }
17
+
14
18
  function runtimeError(code, key) {
15
19
  return new CliError(t(`agent.${key}`), { code });
16
20
  }
@@ -28,7 +32,7 @@ function requireAbsoluteFile(value, code) {
28
32
  if (!stat.isFile() || stat.isSymbolicLink()) {
29
33
  throw runtimeError(code, 'unsafe_file');
30
34
  }
31
- return { path: fs.realpathSync(value), stat };
35
+ return { path: canonicalPath(value), stat };
32
36
  }
33
37
 
34
38
  function readRegularFile(filename, maxBytes, code = 'AGENT_RUNTIME_INVALID_PATH') {
@@ -122,7 +126,7 @@ function resolveProduction(options) {
122
126
  const main = parseStrictJson(readRegularFile(path.join(packageRoot, 'package.json'), MAX_MANIFEST_BYTES).bytes);
123
127
  if (!main.optionalDependencies || main.optionalDependencies[name] !== release.packageVersion) {throw new Error('unapproved dependency');}
124
128
  const localRequire = createRequire(path.join(packageRoot, 'package.json'));
125
- installedRoot = path.dirname(fs.realpathSync(localRequire.resolve(`${name}/package.json`)));
129
+ installedRoot = path.dirname(canonicalPath(localRequire.resolve(`${name}/package.json`)));
126
130
  const metadata = parseStrictJson(readRegularFile(safePackageFile(installedRoot, 'package.json'), MAX_MANIFEST_BYTES).bytes);
127
131
  if (metadata.name !== name || metadata.version !== release.packageVersion ||
128
132
  !Array.isArray(metadata.os) || metadata.os.length !== 1 || metadata.os[0] !== platform ||
@@ -152,4 +156,4 @@ function resolveRuntime(options = {}) {
152
156
  return resolveProduction(options);
153
157
  }
154
158
 
155
- module.exports = { PROTOCOL_VERSION, resolveRuntime, requireAbsoluteFile, readRegularFile, digest, platformPackageName };
159
+ module.exports = { PROTOCOL_VERSION, resolveRuntime, requireAbsoluteFile, readRegularFile, digest, platformPackageName, canonicalPath };
@@ -69,6 +69,7 @@ function launchRuntime(runtime, config, options = {}) {
69
69
  const watchesParent = ['connect', 'run'].includes(config.command);
70
70
  if (watchesParent) {config = process.platform === 'win32'
71
71
  ? { ...config, parentPID: process.pid } : { ...config, parentWatchFD: 3 };}
72
+ const watchesPipe = watchesParent && process.platform !== 'win32';
72
73
  const payload = JSON.stringify(config);
73
74
  if (Buffer.byteLength(payload) > MAX_CONFIG_BYTES) {
74
75
  throw new CliError(t('agent.input_invalid'), { code: 'AGENT_INPUT_INVALID' });
@@ -126,7 +127,13 @@ function launchRuntime(runtime, config, options = {}) {
126
127
  diagnostic('error', 'runtime_action_failed', {
127
128
  command: config.command, component: 'node', errorCode: code, retryable: event.retryable === true,
128
129
  });
129
- failure = new CliError(t('agent.runtime_failed'), { code, details: { retryable: event.retryable === true } });
130
+ const enrollmentMessages = {
131
+ ENROLLMENT_EXPIRED: 'agent.enrollment_expired',
132
+ ENROLLMENT_REJECTED: 'agent.enrollment_rejected',
133
+ ENROLLMENT_CONFLICT: 'agent.enrollment_conflict',
134
+ ENROLLMENT_REQUEST_FAILED: 'agent.enrollment_request_failed',
135
+ };
136
+ failure = new CliError(t(enrollmentMessages[code] || 'agent.runtime_failed'), { code, details: { retryable: event.retryable === true } });
130
137
  stop('SIGTERM');
131
138
  return;
132
139
  }
@@ -137,7 +144,11 @@ function launchRuntime(runtime, config, options = {}) {
137
144
  child = (options.spawn || spawn)(runtime.executable, [config.command, '--stdio-config'], {
138
145
  cwd: config.node.packageRoot,
139
146
  env: runtimeEnvironment(options.env),
140
- stdio: watchesParent ? ['pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],
147
+ stdio: watchesPipe ? ['pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],
148
+ // Escape libuv's kill-on-parent-close job on Windows. The native PID
149
+ // handle monitor owns cancellation/draining; retain our Node handle
150
+ // (no unref) so normal foreground execution still waits for completion.
151
+ detached: process.platform === 'win32' && watchesParent,
141
152
  windowsHide: true,
142
153
  shell: false,
143
154
  });
@@ -149,10 +160,10 @@ function launchRuntime(runtime, config, options = {}) {
149
160
  }
150
161
  signals.on('SIGINT', forwardInterrupt);
151
162
  signals.on('SIGTERM', forwardTerminate);
152
- // Config stdin intentionally reaches EOF after one JSON. This distinct
153
- // pipe remains open for the parent lifetime; Go treats EOF as stop/drain,
163
+ // Config stdin intentionally reaches EOF after one JSON. On Unix this
164
+ // separate pipe remains open for the parent lifetime; Go treats EOF as stop/drain,
154
165
  // not successful completion. Do not send credentials on the watch pipe.
155
- const parentWatch = watchesParent && child.stdio && child.stdio[3];
166
+ const parentWatch = watchesPipe && child.stdio && child.stdio[3];
156
167
  if (parentWatch) {parentWatch.on('error', () => {if (!closed) {fail('AGENT_RUNTIME_PARENT_WATCH_FAILED');}});}
157
168
  child.stdout.on('data', (chunk) => {
158
169
  pending += decoder.write(chunk);
@@ -4,6 +4,8 @@ const { createAuthRef, createYidaClient, unwrapYidaResponse } = require('../core
4
4
  const { t } = require('../core/i18n');
5
5
  const { throwUsage } = require('../core/command-errors');
6
6
  const { fetchFormPageList } = require('./form-navigation');
7
+ const { fetchFormSchemaInfo } = require('./get-form-config');
8
+ const { buildDisplayPageEntryUrls } = require('./application-entry-urls');
7
9
 
8
10
  /** Keep complete runtime URLs; the server verifies tenant and resource ownership. */
9
11
  function normalizeUrl(value, appType) {
@@ -54,7 +56,16 @@ async function resolveEntryUrl(authRef, appType, key, formUuid) {
54
56
  if (key === 'frontend') {
55
57
  if (String(form.formType).toLowerCase() !== 'display') { throw new Error(t('app_entry.frontend_display_required', formUuid)); }
56
58
  if (!form.pathName) { throw new Error(t('app_entry.page_offline', formUuid)); }
57
- return normalizeUrl(`${authRef.baseUrl}/${appType}/custom/${formUuid}`, appType);
59
+ const response = await fetchFormSchemaInfo(authRef, appType, formUuid);
60
+ if (!response || response.success === false || response.__needLogin || response.__csrfExpired) {
61
+ throw new Error(t('app_entry.invalid_response'));
62
+ }
63
+ const config = response.content || response.result || response.data;
64
+ const entry = buildDisplayPageEntryUrls({ appType, formUuid, baseUrl: authRef.baseUrl, config, formType: 'display' });
65
+ if (!entry.standaloneUrl || entry.navigationVerification?.renderNav !== false) {
66
+ throw new Error(t('app_entry.frontend_nav_required', formUuid));
67
+ }
68
+ return normalizeUrl(entry.standaloneUrl, appType);
58
69
  }
59
70
  return normalizeUrl(`${authRef.baseUrl}/${appType}/workbench/${formUuid}`, appType);
60
71
  }
@@ -137,8 +148,10 @@ async function run(args = []) {
137
148
  urls[key] = normalizeUrl(result.accessEntries[key].url, appType);
138
149
  }
139
150
  }
140
- console.log(JSON.stringify({ appType, ...result, urls }, null, 2));
141
- return result;
151
+ const output = { ...result, appType, urls,
152
+ verification: { configurationReadback: true, runtimeVerified: false } };
153
+ console.log(JSON.stringify(output, null, 2));
154
+ return output;
142
155
  }
143
156
 
144
157
  module.exports = { run, parseArgs, normalizeUrl, resolveEntryUrl };
@@ -5,6 +5,7 @@ const { t } = require('../core/i18n');
5
5
  const { buildYidaTitleI18n, normalizeYidaLocale, resolveContentLocale } = require('../core/yida-i18n');
6
6
  const { banner, step, label, success, fail, warn, info, error, result, usage } = require('../core/chalk');
7
7
  const { throwCommandError, throwUsage } = require('../core/command-errors');
8
+ const { readRenderNav } = require('./application-entry-urls');
8
9
 
9
10
  function parseArgs(inputArgs = process.argv.slice(2)) {
10
11
  const args = [...inputArgs];
@@ -158,12 +159,14 @@ async function run(args = process.argv.slice(2)) {
158
159
  success(t('update_form_config.read_ok'));
159
160
 
160
161
  // A keep request must not silently switch navigation when the read is incomplete.
161
- if (keepNav && ![true, false, 'true', 'false'].includes(currentConfig.isRenderNav)) {
162
+ const currentRenderNav = readRenderNav(currentConfig);
163
+ if (keepNav && currentRenderNav === null) {
162
164
  throwCommandError(t('get_form_config.read_failed', t('common.request_failed_label')), {
163
165
  code: 'CONFIG_READ_FAILED',
164
166
  });
165
167
  }
166
- const effectiveNav = keepNav ? String(currentConfig.isRenderNav) : isRenderNav;
168
+ // The read API exposes renderNav; the write API still accepts isRenderNav.
169
+ const effectiveNav = keepNav ? String(currentRenderNav) : isRenderNav;
167
170
 
168
171
  step(3, t('update_form_config.step_update'));
169
172
  info(t('update_form_config.sending_request'));
@@ -472,6 +472,7 @@ function buildFullAppArtifactRoute(manifest) {
472
472
  user_visible_expression_policy: workflow.user_visible_expression_policy,
473
473
  optional_asset_branch: workflow.optional_asset_branch,
474
474
  design_mode_policy: workflow.design_mode_policy,
475
+ plan_confirmation_policy: workflow.plan_confirmation_policy,
475
476
  navigation_policy: workflow.navigation_policy,
476
477
  form_entry_policy: workflow.form_entry_policy,
477
478
  divider_style_contract: workflow.divider_style_contract,