@gajae-code/coding-agent 0.18.10 → 0.18.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +295 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { logger } from "@gajae-code/utils";
|
|
2
|
+
import type { SessionManager } from "../session/session-manager";
|
|
3
|
+
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "../session/streaming-output";
|
|
4
|
+
|
|
5
|
+
const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
|
|
6
|
+
const ASYNC_PREVIEW_MAX_CHARS = 4_000;
|
|
7
|
+
|
|
8
|
+
export async function formatAsyncResultForFollowUp(
|
|
9
|
+
sessionManager: SessionManager,
|
|
10
|
+
result: string,
|
|
11
|
+
allowArtifact = true,
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
if (result.length <= ASYNC_INLINE_RESULT_MAX_CHARS) return result;
|
|
14
|
+
const preview = `${result.slice(0, ASYNC_PREVIEW_MAX_CHARS)}\n\n[Output truncated. Showing first ${ASYNC_PREVIEW_MAX_CHARS.toLocaleString()} characters.]`;
|
|
15
|
+
// Denied owned deliveries must not leave unreferenced output artifacts.
|
|
16
|
+
if (!allowArtifact) return preview;
|
|
17
|
+
try {
|
|
18
|
+
const artifactId = await sessionManager.saveArtifact(result, "async");
|
|
19
|
+
if (artifactId) {
|
|
20
|
+
const bytes = Buffer.byteLength(result, "utf8");
|
|
21
|
+
const omittedBytes =
|
|
22
|
+
bytes > DEFAULT_ARTIFACT_MAX_BYTES
|
|
23
|
+
? bytes - truncateHeadBytes(result, DEFAULT_ARTIFACT_MAX_BYTES).bytes
|
|
24
|
+
: 0;
|
|
25
|
+
const notice = omittedBytes
|
|
26
|
+
? `Saved completion output is incomplete; omitted ${omittedBytes} UTF-8 bytes`
|
|
27
|
+
: "Saved completion output";
|
|
28
|
+
return `${preview}\n${notice}: artifact://${artifactId}`;
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
try {
|
|
32
|
+
logger.warn("Failed to persist async follow-up artifact", {
|
|
33
|
+
error: error instanceof Error ? error.message : String(error),
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
// Diagnostics must not discard a completed job's preview.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return `${preview}\n[Completion output storage unavailable; only the preview is available here.]`;
|
|
40
|
+
}
|
package/src/autoresearch/git.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import { isEnoent } from "@gajae-code/utils";
|
|
16
17
|
import * as git from "../utils/git";
|
|
17
18
|
import { HARNESS_FILENAME, normalizePathSpec } from "./harness";
|
|
18
19
|
|
|
@@ -49,10 +50,16 @@ export function getCurrentAutoresearchBranch(workDir: string): Promise<string |
|
|
|
49
50
|
* Mission state under `.gjc/**` is intentionally NOT listed here: it is
|
|
50
51
|
* runtime-owned and only the sanctioned `gjc autoresearch` CLI writes it.
|
|
51
52
|
*/
|
|
52
|
-
export function isAutoresearchAuthorizedResearchPath(
|
|
53
|
+
export function isAutoresearchAuthorizedResearchPath(cwd: string, rawPath: string): boolean {
|
|
53
54
|
const normalized = normalizePathSpec(rawPath);
|
|
54
55
|
if (normalized === ".") return false;
|
|
55
|
-
|
|
56
|
+
if (normalized !== HARNESS_FILENAME) return false;
|
|
57
|
+
try {
|
|
58
|
+
// Writes follow an existing harness symlink into its referent.
|
|
59
|
+
return !fs.lstatSync(path.join(cwd, HARNESS_FILENAME)).isSymbolicLink();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return isEnoent(error);
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/**
|
|
@@ -32,7 +32,7 @@ Do not edit product source, manifests, or dependencies. The harness must use exi
|
|
|
32
32
|
### Steps
|
|
33
33
|
|
|
34
34
|
1. Inspect the target. Read source, identify what to measure, decide on the workload.
|
|
35
|
-
2. Write `autoresearch.sh` only when it is an explicitly approved research artifact;
|
|
35
|
+
2. Write or edit `autoresearch.sh` with a direct file tool only when it is an explicitly approved research artifact; do not use Bash, Python, or eval to change the harness. Otherwise use an existing benchmark command.
|
|
36
36
|
3. Validate it: invoke `bash autoresearch.sh` through the regular `bash` tool. Confirm it exits 0 and emits at least one `METRIC` line. Iterate on the harness until it does.
|
|
37
37
|
4. Start the mission with `gjc autoresearch` (cold intake: goal, constraints, deliverables, and an explicit mode) or `gjc autoresearch --spec <spec>` (handoff intake). Record the primary metric name and its direction (lower/higher is better) with the mission. The harness baseline is snapshotted at mission start and Phase 2 (the iteration loop) begins.
|
|
38
38
|
|
package/src/capability/fs.ts
CHANGED
|
@@ -9,8 +9,8 @@ const dirCache = new Map<string, fs.Dirent[]>();
|
|
|
9
9
|
export type ReadScope = "project" | "user" | "native";
|
|
10
10
|
|
|
11
11
|
export interface FileIdentity {
|
|
12
|
-
readonly dev:
|
|
13
|
-
readonly ino:
|
|
12
|
+
readonly dev: bigint;
|
|
13
|
+
readonly ino: bigint;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface ReadFileOptions {
|
|
@@ -45,7 +45,7 @@ function isWithinOrEqual(root: string, candidate: string): boolean {
|
|
|
45
45
|
async function isHomeIdentityStable(options?: ReadFileOptions): Promise<boolean> {
|
|
46
46
|
if (!options?.isolatedHome || !options.home || !options.homeIdentity) return true;
|
|
47
47
|
try {
|
|
48
|
-
const current = await fs.promises.stat(options.home);
|
|
48
|
+
const current = await fs.promises.stat(options.home, { bigint: true });
|
|
49
49
|
return current.dev === options.homeIdentity.dev && current.ino === options.homeIdentity.ino;
|
|
50
50
|
} catch {
|
|
51
51
|
return false;
|
|
@@ -69,7 +69,7 @@ async function isUserAgentIdentityStable(options?: ReadFileOptions): Promise<boo
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
try {
|
|
72
|
-
const current = await fs.promises.stat(options.userAgentDir);
|
|
72
|
+
const current = await fs.promises.stat(options.userAgentDir, { bigint: true });
|
|
73
73
|
return current.dev === options.userAgentIdentity.dev && current.ino === options.userAgentIdentity.ino;
|
|
74
74
|
} catch {
|
|
75
75
|
return false;
|
|
@@ -79,7 +79,7 @@ async function isUserAgentIdentityStable(options?: ReadFileOptions): Promise<boo
|
|
|
79
79
|
async function isContainmentRootIdentityStable(options?: ReadFileOptions): Promise<boolean> {
|
|
80
80
|
if (!options?.isolatedHome || !options.containmentRoot || !options.containmentRootIdentity) return true;
|
|
81
81
|
try {
|
|
82
|
-
const current = await fs.promises.stat(options.containmentRoot);
|
|
82
|
+
const current = await fs.promises.stat(options.containmentRoot, { bigint: true });
|
|
83
83
|
return sameFileIdentity(current, options.containmentRootIdentity);
|
|
84
84
|
} catch {
|
|
85
85
|
return false;
|
|
@@ -152,8 +152,8 @@ async function isCurrentIsolatedPath(abs: string, options?: ReadFileOptions): Pr
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
function isSingleLinkRegularFile(stat: fs.Stats): boolean {
|
|
156
|
-
return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1;
|
|
155
|
+
function isSingleLinkRegularFile(stat: fs.Stats | fs.BigIntStats): boolean {
|
|
156
|
+
return stat.isFile() && !stat.isSymbolicLink() && (stat.nlink === 1n || stat.nlink === 1);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** Reject hard-linked executable or configuration files in isolated discovery. */
|
|
@@ -165,8 +165,16 @@ export async function isSingleLinkRegularFileAt(filePath: string): Promise<boole
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
function sameFileIdentity(
|
|
169
|
-
|
|
168
|
+
function sameFileIdentity(
|
|
169
|
+
left: Pick<fs.Stats, "dev" | "ino"> | Pick<fs.BigIntStats, "dev" | "ino">,
|
|
170
|
+
right: Pick<fs.Stats, "dev" | "ino"> | Pick<fs.BigIntStats, "dev" | "ino"> | FileIdentity,
|
|
171
|
+
): boolean {
|
|
172
|
+
// Convert to bigint if needed for comparison
|
|
173
|
+
const leftDev = typeof left.dev === "bigint" ? left.dev : BigInt(left.dev);
|
|
174
|
+
const leftIno = typeof left.ino === "bigint" ? left.ino : BigInt(left.ino);
|
|
175
|
+
const rightDev = typeof right.dev === "bigint" ? right.dev : BigInt(right.dev);
|
|
176
|
+
const rightIno = typeof right.ino === "bigint" ? right.ino : BigInt(right.ino);
|
|
177
|
+
return leftDev === rightDev && leftIno === rightIno;
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
/**
|
|
@@ -174,13 +182,13 @@ function sameFileIdentity(left: Pick<fs.Stats, "dev" | "ino">, right: Pick<fs.St
|
|
|
174
182
|
* Stat first and canonicalize second so a parent replacement in either seam
|
|
175
183
|
* is detected by the descriptor identity check below.
|
|
176
184
|
*/
|
|
177
|
-
async function statAuthorizedPath(abs: string, options?: ReadFileOptions): Promise<fs.Stats | null> {
|
|
185
|
+
async function statAuthorizedPath(abs: string, options?: ReadFileOptions): Promise<fs.Stats | fs.BigIntStats | null> {
|
|
178
186
|
if (!options?.isolatedHome) return null;
|
|
179
187
|
if (!(await isHomeIdentityStable(options))) return null;
|
|
180
188
|
if (!(await isUserAgentIdentityStable(options))) return null;
|
|
181
|
-
let authorized: fs.Stats;
|
|
189
|
+
let authorized: fs.Stats | fs.BigIntStats;
|
|
182
190
|
try {
|
|
183
|
-
authorized = await fs.promises.stat(abs);
|
|
191
|
+
authorized = await fs.promises.stat(abs, { bigint: true });
|
|
184
192
|
} catch {
|
|
185
193
|
return null;
|
|
186
194
|
}
|
|
@@ -191,18 +199,18 @@ async function validateOpenedPath(
|
|
|
191
199
|
handle: fsPromises.FileHandle,
|
|
192
200
|
abs: string,
|
|
193
201
|
options: ReadFileOptions | undefined,
|
|
194
|
-
authorized: fs.Stats | null,
|
|
202
|
+
authorized: fs.Stats | fs.BigIntStats | null,
|
|
195
203
|
kind: "file" | "directory",
|
|
196
204
|
): Promise<boolean> {
|
|
197
205
|
if (!options?.isolatedHome) return true;
|
|
198
206
|
if (!(await isHomeIdentityStable(options))) return false;
|
|
199
207
|
if (!(await isUserAgentIdentityStable(options))) return false;
|
|
200
208
|
try {
|
|
201
|
-
const opened = await handle.stat();
|
|
209
|
+
const opened = await handle.stat({ bigint: true });
|
|
202
210
|
const validKind =
|
|
203
211
|
kind === "file" ? isSingleLinkRegularFile(opened) : opened.isDirectory() && !opened.isSymbolicLink();
|
|
204
212
|
if (!validKind || authorized === null || !sameFileIdentity(opened, authorized)) return false;
|
|
205
|
-
const current = await fs.promises.stat(abs);
|
|
213
|
+
const current = await fs.promises.stat(abs, { bigint: true });
|
|
206
214
|
if (!sameFileIdentity(opened, current)) return false;
|
|
207
215
|
return await isCurrentIsolatedPath(abs, options);
|
|
208
216
|
} catch {
|
|
@@ -212,7 +220,7 @@ async function validateOpenedPath(
|
|
|
212
220
|
|
|
213
221
|
interface OpenedPath {
|
|
214
222
|
handle: fsPromises.FileHandle;
|
|
215
|
-
authorized: fs.Stats | null;
|
|
223
|
+
authorized: fs.Stats | fs.BigIntStats | null;
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
async function openIsolatedFile(abs: string, options?: ReadFileOptions): Promise<OpenedPath | null> {
|
|
@@ -279,7 +287,7 @@ export async function readFile(filePath: string, options?: ReadFileOptions): Pro
|
|
|
279
287
|
/** Capture a stable physical identity for an authorized containment root. */
|
|
280
288
|
export async function capturePathIdentity(filePath: string): Promise<FileIdentity | null> {
|
|
281
289
|
try {
|
|
282
|
-
const stat = await fs.promises.stat(path.resolve(filePath));
|
|
290
|
+
const stat = await fs.promises.stat(path.resolve(filePath), { bigint: true });
|
|
283
291
|
return { dev: stat.dev, ino: stat.ino };
|
|
284
292
|
} catch {
|
|
285
293
|
return null;
|
|
@@ -342,14 +350,14 @@ export async function readFileSize(filePath: string, options?: ReadFileOptions):
|
|
|
342
350
|
async function validateDirectoryPath(
|
|
343
351
|
abs: string,
|
|
344
352
|
options: ReadFileOptions | undefined,
|
|
345
|
-
authorized: fs.Stats | null,
|
|
353
|
+
authorized: fs.Stats | fs.BigIntStats | null,
|
|
346
354
|
): Promise<boolean> {
|
|
347
355
|
if (!options?.isolatedHome) return true;
|
|
348
356
|
if (authorized === null) return false;
|
|
349
357
|
if (!(await isHomeIdentityStable(options))) return false;
|
|
350
358
|
if (!(await isUserAgentIdentityStable(options))) return false;
|
|
351
359
|
try {
|
|
352
|
-
const current = await fs.promises.stat(abs);
|
|
360
|
+
const current = await fs.promises.stat(abs, { bigint: true });
|
|
353
361
|
return (
|
|
354
362
|
current.isDirectory() && sameFileIdentity(current, authorized) && (await isCurrentIsolatedPath(abs, options))
|
|
355
363
|
);
|
package/src/capability/index.ts
CHANGED
|
@@ -482,7 +482,7 @@ export async function loadCapabilityForHome<T>(
|
|
|
482
482
|
// read another profile's SYSTEM/RULES/AGENTS, skills, commands, hooks,
|
|
483
483
|
// settings, or executable descriptors.
|
|
484
484
|
const canonicalHome = await canonicalizeThroughExistingAncestor(resolvedHome);
|
|
485
|
-
const homeStats = await fs.stat(canonicalHome);
|
|
485
|
+
const homeStats = await fs.stat(canonicalHome, { bigint: true });
|
|
486
486
|
const homeIdentity = { dev: homeStats.dev, ino: homeStats.ino };
|
|
487
487
|
const trustedHome = await canonicalizeThroughExistingAncestor(getTrustedHomeDir());
|
|
488
488
|
const isolatedHome = canonicalHome !== trustedHome;
|
|
@@ -510,7 +510,7 @@ export async function loadCapabilityForHome<T>(
|
|
|
510
510
|
"user agent directory",
|
|
511
511
|
)
|
|
512
512
|
: path.join(canonicalHome, getConfigDirName(), "agent");
|
|
513
|
-
const userAgentStats = await fs.stat(userAgentDir).catch(() => null);
|
|
513
|
+
const userAgentStats = await fs.stat(userAgentDir, { bigint: true }).catch(() => null);
|
|
514
514
|
const userAgentIdentity = userAgentStats ? { dev: userAgentStats.dev, ino: userAgentStats.ino } : null;
|
|
515
515
|
await assertExplicitHomeRoots(canonicalHome, cwd, Boolean(options.agentDir), effectiveProviderIds, isolatedHome);
|
|
516
516
|
const repoRootCandidate = await findRepoRoot(cwd, canonicalHome, {
|
package/src/cli/accounts-cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
OAuthCredentialSelectorError,
|
|
15
15
|
type OAuthPinTarget,
|
|
16
16
|
resolveOAuthStorageProvider,
|
|
17
|
+
selectOrganizationMatches,
|
|
17
18
|
} from "@gajae-code/ai/core";
|
|
18
19
|
import { getAgentDir } from "@gajae-code/utils";
|
|
19
20
|
import { ModelRegistry } from "../config/model-registry";
|
|
@@ -25,15 +26,21 @@ import {
|
|
|
25
26
|
clearPersistentPinForRemovedRows,
|
|
26
27
|
} from "../session/account-inventory";
|
|
27
28
|
import { CREDENTIAL_STORE_UNREADABLE_MESSAGE } from "../session/credential-store-errors";
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
type CredentialPreferenceEntryValue,
|
|
31
|
+
isValidCredentialPreferenceCap,
|
|
32
|
+
parsePreferenceAccount,
|
|
33
|
+
resolveStartupAuthConfig,
|
|
34
|
+
type StartupAuthConfigSnapshot,
|
|
35
|
+
} from "../session/startup-auth-config";
|
|
29
36
|
|
|
30
|
-
export const ACCOUNTS_ACTIONS = ["list", "check", "pin", "logout"] as const;
|
|
37
|
+
export const ACCOUNTS_ACTIONS = ["list", "check", "pin", "prefer", "logout"] as const;
|
|
31
38
|
export type AccountsAction = (typeof ACCOUNTS_ACTIONS)[number];
|
|
32
39
|
|
|
33
40
|
export interface AccountsCommandArgs {
|
|
34
41
|
action: AccountsAction;
|
|
35
42
|
provider?: string;
|
|
36
|
-
|
|
43
|
+
selectors?: readonly string[];
|
|
37
44
|
flags: {
|
|
38
45
|
json?: boolean;
|
|
39
46
|
persistent?: boolean;
|
|
@@ -92,9 +99,10 @@ function selectorFromInput(raw: string | undefined): AuthCredentialSelector {
|
|
|
92
99
|
if (/^id:[1-9]\d*$/.test(value)) return { kind: "id", value: value.slice(3) };
|
|
93
100
|
if (/^email:[^@\s]+@[^@\s]+$/.test(value)) return { kind: "email", value: value.slice(6) };
|
|
94
101
|
if (/^account:\S+$/.test(value)) return { kind: "account", value: value.slice(8) };
|
|
102
|
+
if (/^org:\S/.test(value)) return { kind: "org", value: value.slice(4).trim() };
|
|
95
103
|
if (/^[^@\s]+@[^@\s]+$/.test(value)) return { kind: "email", value };
|
|
96
104
|
throw new AccountsCommandError(
|
|
97
|
-
"Pin target must be a bare email or an id:<row-id>, email:<email>,
|
|
105
|
+
"Pin target must be a bare email or an id:<row-id>, email:<email>, account:<account-id>, or org:<organization id or name> selector.",
|
|
98
106
|
);
|
|
99
107
|
}
|
|
100
108
|
|
|
@@ -286,15 +294,171 @@ async function runPin(
|
|
|
286
294
|
});
|
|
287
295
|
}
|
|
288
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Split `<account>=<percent>` into the account and its usage cap. The cap follows the whole account, so an `=` before
|
|
299
|
+
* the last `@` belongs to an email local part (`user=2@example.com`), and a suffix that does not start with a digit is
|
|
300
|
+
* part of the account (account ids may end in `=`); a digit-led suffix must be a valid cap.
|
|
301
|
+
*/
|
|
302
|
+
function parsePreferArgument(raw: string): { account: string; cap: number | undefined } {
|
|
303
|
+
const separator = raw.lastIndexOf("=");
|
|
304
|
+
const rawCap = separator < 0 || separator < raw.lastIndexOf("@") ? "" : raw.slice(separator + 1).trim();
|
|
305
|
+
if (!/^\d/.test(rawCap)) return { account: raw.trim(), cap: undefined };
|
|
306
|
+
const cap = /^\d+(\.\d+)?$/.test(rawCap) ? Number(rawCap) : Number.NaN;
|
|
307
|
+
if (!isValidCredentialPreferenceCap(cap)) {
|
|
308
|
+
throw new AccountsCommandError(
|
|
309
|
+
`Usage cap in ${JSON.stringify(raw)} must be a percent above 0 and at most 100, e.g. me@example.com=75; no preference was written.`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return { account: raw.slice(0, separator).trim(), cap };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Portable config text for a resolved OAuth row: its email, else its account id, whichever no other OAuth row of the
|
|
317
|
+
* provider shares. Numeric row ids are never persisted as preferences; a row with neither unique value is refused.
|
|
318
|
+
* Each candidate text is written only when {@link parsePreferenceAccount} reads it back as the same selector, so a
|
|
319
|
+
* successful write neither fails the next startup nor retargets the row: the email is written bare when that
|
|
320
|
+
* round-trips, else as `email:<address>` (an address such as `account:team@example.com` would otherwise parse as an
|
|
321
|
+
* account id, and `email:team@example.com` would lose its prefix).
|
|
322
|
+
*/
|
|
323
|
+
function portablePreferenceAccount(
|
|
324
|
+
row: CredentialInventoryRecord,
|
|
325
|
+
providerRows: readonly CredentialInventoryRecord[],
|
|
326
|
+
): string | undefined {
|
|
327
|
+
const oauthRows = providerRows.filter(other => other.credentialKind === "oauth");
|
|
328
|
+
const email = row.email?.trim().toLowerCase();
|
|
329
|
+
if (email && oauthRows.filter(other => other.email?.trim().toLowerCase() === email).length === 1) {
|
|
330
|
+
const selector: AuthCredentialSelector = { kind: "email", value: row.email!.trim() };
|
|
331
|
+
const written = [selector.value, `email:${selector.value}`].find(text => readsBackAs(text, selector));
|
|
332
|
+
if (written) return written;
|
|
333
|
+
}
|
|
334
|
+
const accountId = row.accountId?.trim();
|
|
335
|
+
if (accountId && oauthRows.filter(other => other.accountId?.trim() === accountId).length === 1) {
|
|
336
|
+
const written = `account:${accountId}`;
|
|
337
|
+
if (readsBackAs(written, { kind: "account", value: accountId })) return written;
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Whether startup parses `text` as exactly `selector`, kind and value. */
|
|
343
|
+
function readsBackAs(text: string, selector: AuthCredentialSelector): boolean {
|
|
344
|
+
const parsed = parsePreferenceAccount(text);
|
|
345
|
+
return parsed?.kind === selector.kind && parsed.value === selector.value;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function writePersistentPreferences(
|
|
349
|
+
provider: string,
|
|
350
|
+
entries: readonly CredentialPreferenceEntryValue[] | undefined,
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
const settings = await Settings.init();
|
|
353
|
+
await settings.commitAtomicBatchWithCurrent(current => {
|
|
354
|
+
const currentAuth = current.auth === undefined ? {} : asRecord(current.auth);
|
|
355
|
+
const rawPreferences = currentAuth?.credentialPreferences;
|
|
356
|
+
const preferences = rawPreferences === undefined ? {} : asRecord(rawPreferences);
|
|
357
|
+
if (!currentAuth || !preferences) {
|
|
358
|
+
throw new AccountsCommandError("Global auth.credentialPreferences is malformed; no preference was written.");
|
|
359
|
+
}
|
|
360
|
+
const next: Record<string, unknown> = { ...preferences };
|
|
361
|
+
if (entries) next[provider] = entries;
|
|
362
|
+
else delete next[provider];
|
|
363
|
+
return [{ path: "auth.credentialPreferences", op: "set", value: next }];
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function runPrefer(
|
|
368
|
+
providerArg: string | undefined,
|
|
369
|
+
accountArgs: readonly string[] | undefined,
|
|
370
|
+
flags: AccountsCommandArgs["flags"],
|
|
371
|
+
): Promise<void> {
|
|
372
|
+
const provider = canonicalProvider(providerArg);
|
|
373
|
+
const accounts = accountArgs ?? [];
|
|
374
|
+
if (flags.clear && accounts.length > 0) {
|
|
375
|
+
throw new AccountsCommandError("Use either preferred accounts or --clear, not both.");
|
|
376
|
+
}
|
|
377
|
+
if (!flags.clear && accounts.length === 0) {
|
|
378
|
+
throw new AccountsCommandError(
|
|
379
|
+
"Prefer requires one or more accounts in order (e.g. me@example.com shared@example.com=75), or --clear.",
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (flags.clear) {
|
|
383
|
+
await writePersistentPreferences(provider, undefined);
|
|
384
|
+
if (flags.json) writeJson({ ok: true, provider, cleared: true });
|
|
385
|
+
else writeText([`Cleared preferred accounts for ${provider}; selection uses normal ranking.`]);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const requested = accounts.map(parsePreferArgument);
|
|
390
|
+
await withAuthStorageAndModels(async (storage, modelRegistry, startupAuth) => {
|
|
391
|
+
if (
|
|
392
|
+
!modelRegistry.getConfiguredProviderIds().includes(provider) &&
|
|
393
|
+
modelRegistry.getAll().every(model => model.provider !== provider)
|
|
394
|
+
) {
|
|
395
|
+
throw new AccountsCommandError(`Provider ${provider} is not configured; no preference was written.`);
|
|
396
|
+
}
|
|
397
|
+
const providerRows = storage.listCredentialInventory(provider);
|
|
398
|
+
const seenIds = new Set<number>();
|
|
399
|
+
const resolved = requested.map(({ account, cap }) => {
|
|
400
|
+
let target: OAuthPinTarget;
|
|
401
|
+
try {
|
|
402
|
+
target = storage.resolveOAuthPinTarget(provider, selectorFromInput(account));
|
|
403
|
+
} catch (error) {
|
|
404
|
+
throw toAccountsCommandError(error) ?? error;
|
|
405
|
+
}
|
|
406
|
+
if (seenIds.has(target.credentialId)) {
|
|
407
|
+
throw new AccountsCommandError(`${account} is listed more than once; no preference was written.`);
|
|
408
|
+
}
|
|
409
|
+
seenIds.add(target.credentialId);
|
|
410
|
+
const row = providerRows.find(candidate => candidate.id === target.credentialId);
|
|
411
|
+
const portable = row ? portablePreferenceAccount(row, providerRows) : undefined;
|
|
412
|
+
if (!portable) {
|
|
413
|
+
throw new AccountsCommandError(
|
|
414
|
+
`${account} has no unique email or account id to store as a preference; no preference was written.`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
return { account: portable, cap: cap === 100 ? undefined : cap };
|
|
418
|
+
});
|
|
419
|
+
const entries: CredentialPreferenceEntryValue[] = resolved.map(({ account, cap }) =>
|
|
420
|
+
cap === undefined ? account : { [account]: cap },
|
|
421
|
+
);
|
|
422
|
+
await writePersistentPreferences(provider, entries);
|
|
423
|
+
if (flags.json) {
|
|
424
|
+
writeJson({ ok: true, provider, preferences: entries });
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
writeText([
|
|
428
|
+
`Preferred accounts for ${provider}, tried in order:`,
|
|
429
|
+
...resolved.map(
|
|
430
|
+
({ account, cap }, index) =>
|
|
431
|
+
` ${index + 1}. ${account}${cap === undefined ? " (until rate-limited)" : ` (until ${cap}% used)`}`,
|
|
432
|
+
),
|
|
433
|
+
`Then: remaining accounts by ${startupAuth.credentialRankingMode} ranking. Takes effect for new sessions.`,
|
|
434
|
+
]);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
289
438
|
function accountMatches(row: CredentialInventoryRecord, rawAccount: string): boolean {
|
|
290
439
|
const value = rawAccount.trim();
|
|
291
440
|
if (value.length === 0) return false;
|
|
292
441
|
if (/^[1-9]\d*$/.test(value) && row.id === Number(value)) return true;
|
|
293
442
|
if (/^id:[1-9]\d*$/i.test(value) && row.id === Number(value.slice(3))) return true;
|
|
443
|
+
// Labels carry the organization name, so a bare email matches every organization of that account.
|
|
444
|
+
if (row.email?.toLowerCase() === value.toLowerCase()) return true;
|
|
294
445
|
const identity = inventoryIdentity(row);
|
|
295
446
|
return identity?.toLowerCase() === value.toLowerCase();
|
|
296
447
|
}
|
|
297
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Rows a `--account` value designates. `org:` prefers an exact organization-id match over display-name
|
|
451
|
+
* matches; more than one result is reported as ambiguous by the caller, never resolved by guessing.
|
|
452
|
+
*/
|
|
453
|
+
function selectLogoutTargets<T extends { row: CredentialInventoryRecord }>(
|
|
454
|
+
targets: readonly T[],
|
|
455
|
+
rawAccount: string,
|
|
456
|
+
): T[] {
|
|
457
|
+
const value = rawAccount.trim();
|
|
458
|
+
if (/^org:/i.test(value)) return selectOrganizationMatches(targets, value.slice(4), target => target.row);
|
|
459
|
+
return targets.filter(target => accountMatches(target.row, value));
|
|
460
|
+
}
|
|
461
|
+
|
|
298
462
|
function removalCandidates(
|
|
299
463
|
inventory: readonly CredentialInventoryRecord[],
|
|
300
464
|
targets: readonly CredentialRemovalTarget[],
|
|
@@ -335,9 +499,7 @@ async function runLogout(providerArg: string | undefined, flags: AccountsCommand
|
|
|
335
499
|
try {
|
|
336
500
|
const inventory = storage.listCredentialInventory(provider);
|
|
337
501
|
const targets = removalCandidates(inventory, storage.listCredentialRemovalTargets(provider));
|
|
338
|
-
const selected = flags.all
|
|
339
|
-
? targets
|
|
340
|
-
: targets.filter(candidate => accountMatches(candidate.row, flags.account ?? ""));
|
|
502
|
+
const selected = flags.all ? targets : selectLogoutTargets(targets, flags.account ?? "");
|
|
341
503
|
if (selected.length !== 1 && !flags.all) throwLogoutResolutionError(provider, flags.account, targets);
|
|
342
504
|
if (selected.length === 0) throwLogoutResolutionError(provider, flags.account, targets);
|
|
343
505
|
const result = storage.removeAuthCredentialsHard(
|
|
@@ -383,7 +545,13 @@ export async function runAccountsCommand(cmd: AccountsCommandArgs): Promise<void
|
|
|
383
545
|
await runCheck(cmd.provider, cmd.flags);
|
|
384
546
|
return;
|
|
385
547
|
case "pin":
|
|
386
|
-
|
|
548
|
+
if ((cmd.selectors?.length ?? 0) > 1) {
|
|
549
|
+
throw new AccountsCommandError("Pin takes one selector; use `gjc accounts prefer` for an ordered list.");
|
|
550
|
+
}
|
|
551
|
+
await runPin(cmd.provider, cmd.selectors?.[0], cmd.flags);
|
|
552
|
+
return;
|
|
553
|
+
case "prefer":
|
|
554
|
+
await runPrefer(cmd.provider, cmd.selectors, cmd.flags);
|
|
387
555
|
return;
|
|
388
556
|
case "logout":
|
|
389
557
|
await runLogout(cmd.provider, cmd.flags);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
AuthStorage,
|
|
26
26
|
type CredentialDisabledEvent,
|
|
27
27
|
DEFAULT_AUTH_BROKER_BIND,
|
|
28
|
+
formatOAuthIdentityLabel,
|
|
28
29
|
getEnvApiKey,
|
|
29
30
|
getOAuthProviders,
|
|
30
31
|
listProvidersWithEnvKey,
|
|
@@ -568,19 +569,21 @@ interface MigrateSkip {
|
|
|
568
569
|
|
|
569
570
|
function credentialIdentity(provider: string, credential: AuthCredential): string {
|
|
570
571
|
if (credential.type === "api_key") return "(api key)";
|
|
571
|
-
return credential
|
|
572
|
+
return formatOAuthIdentityLabel(credential) ?? `<${provider} oauth>`;
|
|
572
573
|
}
|
|
573
574
|
|
|
574
575
|
/**
|
|
575
576
|
* Build the set of "identities already on the broker" so re-runs are idempotent.
|
|
576
|
-
* For OAuth, identity = email|accountId|projectId
|
|
577
|
-
*
|
|
578
|
-
*
|
|
577
|
+
* For OAuth, identity = email|accountId|projectId, scoped to the organization
|
|
578
|
+
* when the credential carries one (one account can sign in to several
|
|
579
|
+
* organizations). For api_key, we collapse to a single marker per provider
|
|
580
|
+
* (broker has no concept of "multiple api keys per provider with different
|
|
581
|
+
* identities"; upsert would coalesce them).
|
|
579
582
|
*/
|
|
580
583
|
function indexBrokerSnapshot(snapshot: {
|
|
581
584
|
credentials: Array<{
|
|
582
585
|
provider: string;
|
|
583
|
-
credential: { type: string; email?: string; accountId?: string; projectId?: string };
|
|
586
|
+
credential: { type: string; email?: string; accountId?: string; projectId?: string; organizationId?: string };
|
|
584
587
|
}>;
|
|
585
588
|
}): Map<string, Set<string>> {
|
|
586
589
|
const out = new Map<string, Set<string>>();
|
|
@@ -589,22 +592,29 @@ function indexBrokerSnapshot(snapshot: {
|
|
|
589
592
|
if (entry.credential.type === "api_key") {
|
|
590
593
|
ids.add("@api_key");
|
|
591
594
|
} else {
|
|
592
|
-
|
|
593
|
-
if (entry.credential.
|
|
594
|
-
if (entry.credential.
|
|
595
|
+
const org = organizationSuffix(entry.credential.organizationId);
|
|
596
|
+
if (entry.credential.email) ids.add(`email:${entry.credential.email}${org}`);
|
|
597
|
+
if (entry.credential.accountId) ids.add(`accountId:${entry.credential.accountId}${org}`);
|
|
598
|
+
if (entry.credential.projectId) ids.add(`projectId:${entry.credential.projectId}${org}`);
|
|
595
599
|
}
|
|
596
600
|
out.set(entry.provider, ids);
|
|
597
601
|
}
|
|
598
602
|
return out;
|
|
599
603
|
}
|
|
600
604
|
|
|
605
|
+
function organizationSuffix(organizationId: string | undefined): string {
|
|
606
|
+
const trimmed = organizationId?.trim();
|
|
607
|
+
return trimmed ? `|org:${trimmed}` : "";
|
|
608
|
+
}
|
|
609
|
+
|
|
601
610
|
function brokerAlreadyHas(existing: Map<string, Set<string>>, provider: string, credential: AuthCredential): boolean {
|
|
602
611
|
const ids = existing.get(provider);
|
|
603
612
|
if (!ids) return false;
|
|
604
613
|
if (credential.type === "api_key") return ids.has("@api_key");
|
|
605
|
-
|
|
606
|
-
if (credential.
|
|
607
|
-
if (credential.
|
|
614
|
+
const org = organizationSuffix(credential.organizationId);
|
|
615
|
+
if (credential.email && ids.has(`email:${credential.email}${org}`)) return true;
|
|
616
|
+
if (credential.accountId && ids.has(`accountId:${credential.accountId}${org}`)) return true;
|
|
617
|
+
if (credential.projectId && ids.has(`projectId:${credential.projectId}${org}`)) return true;
|
|
608
618
|
return false;
|
|
609
619
|
}
|
|
610
620
|
|
|
@@ -14,7 +14,7 @@ import { randomUUID } from "node:crypto";
|
|
|
14
14
|
import { realpathSync } from "node:fs";
|
|
15
15
|
import * as fs from "node:fs/promises";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
-
import { isCompiledBinary, VERSION } from "@gajae-code/utils";
|
|
17
|
+
import { isCompiledBinary, logger, VERSION } from "@gajae-code/utils";
|
|
18
18
|
import { isUpdateChannel } from "../../config/update-channel";
|
|
19
19
|
import {
|
|
20
20
|
activationRecordPath,
|
|
@@ -27,8 +27,10 @@ import {
|
|
|
27
27
|
snapshotRegularFile,
|
|
28
28
|
} from "../install-activation";
|
|
29
29
|
import {
|
|
30
|
+
ActivationRetryCandidateRepointError,
|
|
30
31
|
acquireBinaryUpdateLock,
|
|
31
32
|
fetchAndVerifyOfficialPinnedCandidate,
|
|
33
|
+
InstallationAccessDeniedError,
|
|
32
34
|
isProtectedSourcePathForInstall,
|
|
33
35
|
replaceBinaryForUpdate,
|
|
34
36
|
smokeTestPinnedCandidate,
|
|
@@ -111,8 +113,10 @@ export interface InstallRepairResult {
|
|
|
111
113
|
readonly reason?: string;
|
|
112
114
|
readonly recordPath?: string;
|
|
113
115
|
readonly sideEffectStarted: boolean;
|
|
114
|
-
/**
|
|
116
|
+
/** Native Windows status returned by exact replacement. */
|
|
115
117
|
readonly windowsErrorCode?: string;
|
|
118
|
+
readonly stagingCleanupWarning?: string;
|
|
119
|
+
readonly retainedPaths?: InstallationAccessDeniedError["retainedPaths"];
|
|
116
120
|
readonly before?: { readonly identity?: FileIdentity; readonly digest?: string };
|
|
117
121
|
readonly after?: { readonly identity?: FileIdentity; readonly digest?: string; readonly version?: string };
|
|
118
122
|
}
|
|
@@ -317,17 +321,43 @@ export async function repairStandaloneBinary(
|
|
|
317
321
|
};
|
|
318
322
|
} catch (error) {
|
|
319
323
|
const pendingActivation = error instanceof Error && error.message === "installation_pending_activation";
|
|
324
|
+
const accessDeniedError = error instanceof InstallationAccessDeniedError ? error : undefined;
|
|
325
|
+
const repointError = error instanceof ActivationRetryCandidateRepointError ? error : undefined;
|
|
320
326
|
const reason = pendingActivation
|
|
321
327
|
? "sharing_violation"
|
|
322
|
-
:
|
|
323
|
-
?
|
|
324
|
-
:
|
|
328
|
+
: accessDeniedError
|
|
329
|
+
? accessDeniedError.code
|
|
330
|
+
: repointError
|
|
331
|
+
? repointError.reasonCode
|
|
332
|
+
: error instanceof Error && /^[a-z][a-z0-9_]{0,63}$/.test(error.message)
|
|
333
|
+
? error.message
|
|
334
|
+
: "installation_execution_unverified";
|
|
335
|
+
if (accessDeniedError)
|
|
336
|
+
logger.warn("Doctor standalone binary restore failed with access denied", {
|
|
337
|
+
targetPath,
|
|
338
|
+
code: accessDeniedError.code,
|
|
339
|
+
...(accessDeniedError.windowsErrorCode ? { windowsErrorCode: accessDeniedError.windowsErrorCode } : {}),
|
|
340
|
+
...(accessDeniedError.stagingCleanupWarning
|
|
341
|
+
? { stagingCleanupWarning: accessDeniedError.stagingCleanupWarning }
|
|
342
|
+
: {}),
|
|
343
|
+
...(accessDeniedError.retainedPaths ? { retainedPaths: accessDeniedError.retainedPaths } : {}),
|
|
344
|
+
});
|
|
345
|
+
if (repointError)
|
|
346
|
+
logger.warn("Doctor standalone binary restore retained a retry candidate after record repoint failed", {
|
|
347
|
+
path: repointError.candidatePath,
|
|
348
|
+
reason: repointError.reasonCode,
|
|
349
|
+
});
|
|
325
350
|
return {
|
|
326
351
|
state: pendingActivation ? "pending_activation" : started ? "failed" : "blocked",
|
|
327
352
|
reason,
|
|
328
353
|
sideEffectStarted: started,
|
|
329
354
|
before,
|
|
330
355
|
recordPath,
|
|
356
|
+
...(accessDeniedError?.windowsErrorCode ? { windowsErrorCode: accessDeniedError.windowsErrorCode } : {}),
|
|
357
|
+
...(accessDeniedError?.stagingCleanupWarning
|
|
358
|
+
? { stagingCleanupWarning: accessDeniedError.stagingCleanupWarning }
|
|
359
|
+
: {}),
|
|
360
|
+
...(accessDeniedError?.retainedPaths ? { retainedPaths: accessDeniedError.retainedPaths } : {}),
|
|
331
361
|
};
|
|
332
362
|
} finally {
|
|
333
363
|
await release();
|
package/src/cli/fast-help.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function getExtraHelpText(): string {
|
|
|
8
8
|
${APP_NAME} session - List, inspect, create, remove, or attach sessions
|
|
9
9
|
${APP_NAME} state - Inspect or manage persisted GJC state
|
|
10
10
|
${APP_NAME} auth-broker - Manage the auth-broker (credential vault)
|
|
11
|
-
${APP_NAME} accounts - List, check, pin, and remove stored OAuth accounts
|
|
11
|
+
${APP_NAME} accounts - List, check, pin, prefer, and remove stored OAuth accounts
|
|
12
12
|
${APP_NAME} models presets - Inspect, refresh, pin, rollback, or disable the signed model preset registry
|
|
13
13
|
${APP_NAME} auth-gateway - Run an auth-gateway forward proxy
|
|
14
14
|
${APP_NAME} harness - Run harness control-plane commands
|