@jc_stack/ez-agents 0.1.0-beta.47 → 0.1.0-beta.49
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/.env.example +3 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +30 -0
- package/Dockerfile +5 -1
- package/THIRD_PARTY_NOTICES.md +6 -3
- package/bin/ez +7 -0
- package/docker/broker-readiness.mjs +54 -0
- package/docker/broker-update-smoke.mjs +100 -0
- package/docker/run.ts +1 -1
- package/docker/upgrade-smoke.mjs +1 -1
- package/docs/application-channel.md +32 -1
- package/docs/architecture/ai-selection.md +23 -4
- package/docs/docker-runtime.md +57 -0
- package/docs/isolated-plugin-update-readiness.md +66 -0
- package/docs/releasing.md +4 -0
- package/docs/scheduling.md +141 -6
- package/docs/selective-monitoring.md +13 -0
- package/docs/upgrades.md +33 -4
- package/docs/work-preflight.md +9 -0
- package/package.json +1 -1
- package/src/ai-cli.ts +28 -15
- package/src/ai.ts +98 -15
- package/src/application-channel.ts +52 -12
- package/src/application-cli.ts +7 -1
- package/src/application-origin.ts +2 -0
- package/src/audio.ts +37 -22
- package/src/auth-profile.ts +53 -0
- package/src/client-defaults.ts +3 -2
- package/src/codex-session.ts +14 -3
- package/src/config.ts +9 -0
- package/src/control-state.ts +31 -22
- package/src/delivery-socket-client.d.mts +2 -0
- package/src/delivery-socket-client.mjs +2 -0
- package/src/delivery-socket.ts +26 -1
- package/src/event-sources.ts +2 -2
- package/src/executor.ts +81 -7
- package/src/files.ts +47 -3
- package/src/host-executor.ts +68 -34
- package/src/index.ts +227 -110
- package/src/install-tools.mjs +8 -5
- package/src/menu.ts +9 -7
- package/src/plugin-broker.mjs +50 -14
- package/src/plugins/manager.mjs +34 -8
- package/src/run-evidence.ts +78 -0
- package/src/runs.ts +24 -2
- package/src/schedule-cli.ts +100 -13
- package/src/schedule-preflight.ts +23 -0
- package/src/scheduled-tasks.ts +16 -12
- package/src/scheduler.ts +85 -19
- package/src/scripts.ts +167 -0
- package/src/setup-fallback.ts +122 -0
- package/src/setup.ts +2 -2
- package/src/task-capability.ts +17 -1
- package/src/task-executor.ts +13 -9
- package/src/task-mcp.ts +20 -8
- package/src/tasks.ts +89 -9
- package/src/updates/runtime.mjs +107 -30
- package/src/updates/supervisor.mjs +27 -6
- package/test/ai.test.ts +40 -0
- package/test/application-channel.test.ts +16 -1
- package/test/audio.test.ts +19 -0
- package/test/auth-profiles.test.ts +187 -0
- package/test/config.test.ts +8 -0
- package/test/executor.test.ts +37 -0
- package/test/files.test.ts +44 -0
- package/test/host-executor.test.ts +45 -2
- package/test/install-json.test.mjs +21 -0
- package/test/plugin-broker.test.mjs +67 -0
- package/test/plugin-manager.test.mjs +44 -0
- package/test/private-speech-config.test.ts +22 -0
- package/test/run-evidence.test.ts +78 -0
- package/test/schedule-preflight.test.ts +49 -0
- package/test/scheduler-host.test.ts +7 -0
- package/test/scheduler-relay.test.ts +38 -3
- package/test/scheduler.test.ts +1 -1
- package/test/script-schedules.test.ts +219 -0
- package/test/setup-fallback.test.ts +245 -0
- package/test/task-native.test.ts +50 -12
- package/test/tasks.test.ts +97 -4
- package/test/updates.test.mjs +100 -11
package/docs/scheduling.md
CHANGED
|
@@ -55,6 +55,112 @@ Paused, stale-owner, overlapping or held tasks and unmet conditions cannot be
|
|
|
55
55
|
triggered. Historical deferred tasks keep their source context
|
|
56
56
|
available through `ezenciel-agents-schedule context`.
|
|
57
57
|
|
|
58
|
+
## Ordered setup fallback
|
|
59
|
+
|
|
60
|
+
An agent task may list up to three fallback setups after its saved one, for
|
|
61
|
+
example `claude · opus → claude · sonnet → codex · gpt-6-sol`:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
ezenciel-agents-schedule create --name Daily --cli claude --model opus \
|
|
65
|
+
--cron '0 9 * * *' --timezone Asia/Dubai --text 'Prepare the daily report.' \
|
|
66
|
+
--fallback cli=claude,model=sonnet,effort=medium --fallback cli=codex,model=gpt-6-sol
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`edit` keeps the saved fallbacks unless `--fallback` replaces them or
|
|
70
|
+
`--clear-fallbacks` removes them. Setups must be distinct and each needs a model.
|
|
71
|
+
|
|
72
|
+
Core advances to the next setup only with typed evidence that nothing ran: the
|
|
73
|
+
CLI is not installed or executable, or the provider rejected the turn before any
|
|
74
|
+
model output, tool or hook item (quota/rate limit, billing, overload, server
|
|
75
|
+
error, unavailable model or bad request). It stops, without replay, on access
|
|
76
|
+
denial (authentication, account or organization), cancellation, relay shutdown,
|
|
77
|
+
a schedule edit, a policy or unknown error, an interrupted transport, or any
|
|
78
|
+
failure after work began. Evidence comes from structured events only: Codex
|
|
79
|
+
native-session `codexErrorInfo` and Claude `stream-json` error fields (scheduled
|
|
80
|
+
Claude runs use `--output-format stream-json`; the host forwards only a compact
|
|
81
|
+
startup summary). Other engines advance only when their CLI is unavailable.
|
|
82
|
+
|
|
83
|
+
A quota rejection skips the remaining setups in the same quota scope: the native
|
|
84
|
+
client (`claude`, `codex` including desktop and custom Codex providers, `grok`)
|
|
85
|
+
or the upstream provider for OpenCode/Pi models. A different credential alone is
|
|
86
|
+
never treated as an independent allowance. Each setup runs at most once per
|
|
87
|
+
occurrence.
|
|
88
|
+
|
|
89
|
+
Every attempt keeps the same run ID, occurrence, schedule revision, workspace,
|
|
90
|
+
delivery binding and scheduled concurrency slot, in a fresh native session.
|
|
91
|
+
`run RUN_ID` shows `attempts`: each setup, quota scope, outcome, failure category
|
|
92
|
+
(`cli-unavailable`, `quota`, `provider-rejected`, `access-denied`,
|
|
93
|
+
`after-work-began`, `uncertain`, or `same-quota-scope` for a skipped setup), exit
|
|
94
|
+
code and reported reset time. If every setup is unavailable the run fails with
|
|
95
|
+
reason `setups-unavailable`, a summary of each setup, and an owner notice on
|
|
96
|
+
Telegram-bound tasks. Core never purchases credits, enables overage or consumes
|
|
97
|
+
reset credits; the next regular occurrence runs normally.
|
|
98
|
+
|
|
99
|
+
## Registered script schedules
|
|
100
|
+
|
|
101
|
+
A schedule has one execution type. **Agent** schedules run the saved prompt with
|
|
102
|
+
their saved engine, model and effort, as described below. **Script** schedules run
|
|
103
|
+
a registered, agent-owned script directly: no model, effort, prompt or native
|
|
104
|
+
session is involved.
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
ezenciel-agents-schedule script register book-freshness \
|
|
108
|
+
--file scripts/book-freshness.mjs --interpreter node [--arg=VALUE ...] [--timeout-seconds 900]
|
|
109
|
+
ezenciel-agents-schedule create actual-book-freshness-guard \
|
|
110
|
+
--script book-freshness [--arg=VALUE ...] --cron '15 * * * *' --timezone Asia/Dubai
|
|
111
|
+
ezenciel-agents-schedule script list | show ID | update ID [...] | remove ID
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The agent writes, tests and maintains the script; core owns registration,
|
|
115
|
+
scheduling, authorization, process execution, cancellation and run receipts.
|
|
116
|
+
A registration records the owner binding, a workspace-relative entry point, an
|
|
117
|
+
installed interpreter (command name on the executor PATH or absolute path),
|
|
118
|
+
literal arguments, a timeout and the entry point's SHA-256 under a new revision.
|
|
119
|
+
It references the workspace file; core keeps no source copy. Paths outside the
|
|
120
|
+
workspace (including symlink escapes) and shell command strings are rejected.
|
|
121
|
+
Core invokes `interpreter entry registrationArgs... scheduleArgs...` directly,
|
|
122
|
+
without a shell. Missing interpreters fail clearly; nothing is installed.
|
|
123
|
+
|
|
124
|
+
When an occurrence is queued, the run captures the current registration
|
|
125
|
+
revision and hash. Immediately before spawning, the executor requires that
|
|
126
|
+
revision to be current and the entry-point bytes to match. Changed bytes fail
|
|
127
|
+
the run until the agent explicitly runs `script update`. The hash identifies the
|
|
128
|
+
entry-point file only; it does not certify imported modules or other files.
|
|
129
|
+
|
|
130
|
+
Script runs use the existing scheduler, admission queue, scheduled concurrency
|
|
131
|
+
limit, one-occurrence-per-schedule rule, run records, delivery binding, manual
|
|
132
|
+
`trigger`, pause/resume/edit/remove and `cancel`. They run in the agent's
|
|
133
|
+
isolated environment (relay or host transport) under its normal identity, with
|
|
134
|
+
the executor environment allowlist plus `EZ_RUN_ID`, `EZ_SCHEDULE_ID`,
|
|
135
|
+
`EZ_SCHEDULE_REVISION`, `EZ_DUE_AT`, `EZ_SCRIPT_ID`, `EZ_SCRIPT_REVISION` and
|
|
136
|
+
`EZ_SCRIPT_SHA256`. Use the run/schedule/due identifiers to form a stable
|
|
137
|
+
occurrence identity. External operations go through the bound plugin CLI; a
|
|
138
|
+
script must not read private databases, call providers directly or acquire
|
|
139
|
+
credentials. A script run can read schedule and script state but cannot change
|
|
140
|
+
registrations or schedules.
|
|
141
|
+
|
|
142
|
+
A script runs with the agent's runtime identity and file access, but not inside
|
|
143
|
+
a native engine's own sandbox (for example host Codex `workspace-write`); register
|
|
144
|
+
only code the agent would be allowed to run itself. An interpreter inside the
|
|
145
|
+
workspace is rejected because it would be unhashed code.
|
|
146
|
+
|
|
147
|
+
Each registration has a bounded timeout (default 900 seconds, maximum 21600)
|
|
148
|
+
that does not affect agent runs. Timeout and `cancel` stop the process and its
|
|
149
|
+
children. Failed, timed-out or interrupted script runs are not retried, do not
|
|
150
|
+
fall back to an LLM and do not start a debugging agent. Inspect them with
|
|
151
|
+
`runs`, `run RUN_ID`, `failures` and `evidence`: the run shows the script ID,
|
|
152
|
+
revision and hash, queue/start/finish clocks, exit code, cancellation/timeout,
|
|
153
|
+
failure reason and a 4 KiB redacted tail of stdout/stderr. Those records follow
|
|
154
|
+
the normal relay-memory retention. Stdout is diagnostic only. Process success
|
|
155
|
+
means the script exited 0; domain success still requires the owning plugin's
|
|
156
|
+
validation, receipt and canonical readback, which the script performs and
|
|
157
|
+
records. Notifications use `ezenciel-agents-message` under the normal policy;
|
|
158
|
+
routine unchanged runs should stay quiet.
|
|
159
|
+
|
|
160
|
+
Editing a schedule replaces it completely, so it may switch execution type.
|
|
161
|
+
When replacing an agent schedule with a script, move any judgment the prompt
|
|
162
|
+
performed to its owning workflow first; the script schedule does not carry it.
|
|
163
|
+
|
|
58
164
|
## Execution and authority
|
|
59
165
|
|
|
60
166
|
Due occurrences enter the durable queue with stable IDs and literal task text.
|
|
@@ -63,17 +169,20 @@ No identity files or role instructions are generated. Existing workspace Markdow
|
|
|
63
169
|
provides context, including native `AGENTS.md` discovery and relative links; the
|
|
64
170
|
engine chooses what to read. Per-run native state stays under `control/`.
|
|
65
171
|
|
|
66
|
-
One foreground turn and
|
|
67
|
-
|
|
68
|
-
|
|
172
|
+
One foreground turn and up to six owner-scheduled turns can run at the same time.
|
|
173
|
+
Set `EZ_SCHEDULED_CONCURRENCY` to a positive integer to change the scheduled-job
|
|
174
|
+
limit; it defaults to six. For Docker deployments, put this setting in the
|
|
175
|
+
existing private relay environment file (`EZ_RELAY_ENV_FILE`); no Compose
|
|
176
|
+
layout migration is needed. Foreground inputs queue behind foreground work;
|
|
177
|
+
scheduled turns queue only when their limit is occupied. A long scheduled goal therefore does not block chat. Both streams
|
|
69
178
|
use the existing admission and executor path, with separate native sessions.
|
|
70
179
|
They share the agent-owned Markdown workspace: the agent coordinates shared file
|
|
71
180
|
updates, and plugins enforce canonical record-write guards. The host keeps plugin
|
|
72
|
-
workspace invocations excluded until
|
|
181
|
+
workspace invocations excluded until all native turns finish.
|
|
73
182
|
The agent decides when to send through the message CLI.
|
|
74
183
|
A recurring schedule has at most one pending or active occurrence. Shared
|
|
75
|
-
provider resources still need writer coordination.
|
|
76
|
-
|
|
184
|
+
provider resources still need writer coordination. This is admission to the existing native executor, not a second queue or
|
|
185
|
+
workflow controller.
|
|
77
186
|
|
|
78
187
|
Production relay/host execution has no wall-clock timeout. The old
|
|
79
188
|
`EZ_EXECUTOR_TIMEOUT_SECONDS` setting is ignored. Individual network/tool waits
|
|
@@ -170,6 +279,32 @@ Create a normal recurring schedule with `--every-seconds 900 --when unreviewed-f
|
|
|
170
279
|
|
|
171
280
|
`failures [--all] [--limit N]` returns failedAt, reason, exit code, native session, captured error and runtime versions. Capture keeps at most 4 KiB of redacted stderr, including a failed native Codex turn's error message when supplied. A failure without error details remains explicitly unknown; historical failures are not backfilled. `run RUN_ID` reads an owned run. `review RUN_ID --failed-at ISO --status resolved|attention --diagnosis TEXT --recovery TEXT --outcome TEXT` records the investigation without rewriting execution history. A stale timestamp is rejected; a later failure needs a new review. Restricted reply, external and isolated-task callers cannot review failures. An attention review is handed off, not repeatedly relaunched; another new failure wakes the next review.
|
|
172
281
|
|
|
282
|
+
For a shareable operational projection, use `evidence [RUN_ID] --offset 0 --limit 100`.
|
|
283
|
+
This reads the live relay through its existing authenticated socket, under the
|
|
284
|
+
bound operator or an active unrestricted Telegram owner run. Application/channel,
|
|
285
|
+
external and isolated-task callers cannot use it. It exposes only current-owner
|
|
286
|
+
Telegram foreground/scheduled runs, stable IDs, clocks/status/presets, failure
|
|
287
|
+
metadata without prose, and retained run-associated outbox metadata. No native
|
|
288
|
+
session, prompt, reasoning, tool arguments/results, message text or channel identity
|
|
289
|
+
is returned. Failure text is represented by its SHA-256, not copied or diagnosed.
|
|
290
|
+
|
|
291
|
+
The response carries exact `readStartedAt`/`observedAt`, `snapshotSha256`, and
|
|
292
|
+
coverage total/offset/limit/returned/nextOffset. Follow `nextOffset` with the same
|
|
293
|
+
`--expected SNAPSHOT_SHA256`; changed retained evidence fails closed and requires
|
|
294
|
+
a fresh traversal. Each page recomputes the projection; no snapshot store or
|
|
295
|
+
background capture is added. History resets on relay restart and is bounded to
|
|
296
|
+
2000 terminal runs and 2000 terminal outbox items; full historical coverage cannot
|
|
297
|
+
be inferred. Unassociated deliveries and excluded scopes remain unknown.
|
|
298
|
+
|
|
299
|
+
Delivery records identify queued/sent/failed/unknown state and whether a retained
|
|
300
|
+
Telegram receipt contains a valid clock and returned message IDs (only their count
|
|
301
|
+
is exported). The submitted UTF-8 text/voice-text hash can be compared to an exact
|
|
302
|
+
canonical source. It does not prove rendered or current recipient-side content;
|
|
303
|
+
attachments are not exported. Empty receipt lists never prove that no delivery
|
|
304
|
+
occurred. The relay does not retain native tool/delegation events, so that coverage
|
|
305
|
+
is explicitly unavailable. This command does not mark failures reviewed, send
|
|
306
|
+
messages, read private sessions, trigger work or certify owning results.
|
|
307
|
+
|
|
173
308
|
The prompt controls diagnosis, authorized recovery and quiet notification behavior. Inspect prior effects and receipts before retrying anything. A failed review run itself remains visible as a new failure for the next occurrence.
|
|
174
309
|
|
|
175
310
|
The Telegram Scheduled tasks menu lists enabled schedules that still have a pending
|
|
@@ -178,3 +178,16 @@ After revocation/expiry no subsequent reply may dispatch. Keep these checks
|
|
|
178
178
|
agent-owned: the owner supplies only necessary confirmation, QR scan if needed,
|
|
179
179
|
and the test contact/message. Do not bypass a missing grant by polling the inbox
|
|
180
180
|
in an unrestricted scheduled owner session or sending through the raw CLI.
|
|
181
|
+
|
|
182
|
+
Task `send` accepts `voice: true` for spoken replies up to 1,000 characters on sources advertising `taskVoice`. It uses the relay’s configured speech renderer, rechecks authorization after generation, and preserves the same uncertain-send key contract. Synthesis failure happens before the provider is contacted: it returns an error, sends no text and stores no operation, so the same key may be retried. Voice and file attachments cannot be combined.
|
|
183
|
+
|
|
184
|
+
### Incoming files and images
|
|
185
|
+
|
|
186
|
+
The restricted MCP `read_attachment` tool accepts only an `incomingId` from the
|
|
187
|
+
current run's correspondence. The channel supplies private bounded bytes through
|
|
188
|
+
`task-document`, bound to its account and exact conversation. Core reuses
|
|
189
|
+
`stageChatAttachment` validation (10 MiB) and returns UTF-8 TXT/Markdown or
|
|
190
|
+
PDF text via Poppler, at most 256000 bytes. JPEG/PNG/WebP images return their
|
|
191
|
+
original bounded pixels as MCP image content to the native engine, never an
|
|
192
|
+
extra inference call or a base64 text prompt. It never accepts an owner file path.
|
|
193
|
+
MP4 clips up to 10 MiB and 120 seconds return at most eight sampled JPEG frames, 640 pixels wide and at most 1 MiB combined, through the same native image content. Speech transcripts supplied by the source remain in incoming context. The audited native CLI accepts text and images, not raw video/audio; sampled frames do not capture every motion. Scanned PDFs with no text layer need OCR and return a clear limitation.
|
package/docs/upgrades.md
CHANGED
|
@@ -16,7 +16,7 @@ automatic channel for core and plugins without a saved policy. Existing explicit
|
|
|
16
16
|
stable or manual policies are preserved. The owner may select either per target.
|
|
17
17
|
The main target and installed plugins version independently.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Core upgrades leave channel admission open while native work runs, then pause it after work is idle and recheck races. Plugin upgrades build while native work continues. Activation closes the existing registry admission lock, drains in-flight plugin command leases, and replaces the plugin under that lock. New native plugin calls wait for admission; an outdated revision fails before provider execution and must be rediscovered. No admitted provider operation is replayed. The shared broker and native sessions remain running. Core upgrades still require the full native-work drain. A plugin update also retains that drain if an operator replaced the broker’s single-file host binding (for example to approve new network pins), because that case still requires broker recreation.
|
|
20
20
|
|
|
21
21
|
The owner chooses the saved update policy. The host supervisor owns
|
|
22
22
|
interruption-safe replacement. There is one host service per agent, not a second
|
|
@@ -93,6 +93,12 @@ same candidate again and apply the new job; do not edit the old receipt. Only
|
|
|
93
93
|
Finish the requesting turn after queuing either operation. Verify the main relay
|
|
94
94
|
before requesting a separate plugin upgrade.
|
|
95
95
|
|
|
96
|
+
Codex version discovery parses machine-readable stdout separately from package
|
|
97
|
+
manager notices on stderr. Invalid stdout fails with a named discovery error;
|
|
98
|
+
it is never repaired by stripping arbitrary lines. A historical generic JSON
|
|
99
|
+
parse error alone does not establish which command or output stream caused it.
|
|
100
|
+
Reconcile the job's completed stages and installed state before retrying.
|
|
101
|
+
|
|
96
102
|
## Agent interface
|
|
97
103
|
|
|
98
104
|
Telegram `/status` shows the relay's loaded version, fresh host version and
|
|
@@ -147,9 +153,7 @@ is required. Stable automation excludes prereleases, major changes and 0.x minor
|
|
|
147
153
|
changes. Beta policy permits compatible prereleases. Downgrades are rejected;
|
|
148
154
|
rollback is a recovery operation for the previous installation only.
|
|
149
155
|
|
|
150
|
-
`apply` records one durable queued job. **
|
|
151
|
-
queuing it.** It must not wait for completion in that same turn. The supervisor
|
|
152
|
-
pauses new run admission, drains already-started work, then consumes the job.
|
|
156
|
+
`apply` records one durable queued job. **For a core upgrade, the agent must finish its turn after queuing it** so the supervisor can drain native work. A plugin upgrade can complete while that turn continues; only plugin-call admission is held during activation. Calls already executing finish first.
|
|
153
157
|
One job at a time; an unresolved recovery blocks new upgrades. The agent processes
|
|
154
158
|
multiple requested packages over successive update requests.
|
|
155
159
|
|
|
@@ -285,3 +289,28 @@ the host or create a repair task. An automatically failed or rolled-back version
|
|
|
285
289
|
is not retried until a newer version appears or the owner explicitly retries.
|
|
286
290
|
Inspect `ez updates check` and `ez updates status` for target diagnostics.
|
|
287
291
|
Actual update transactions retain their existing admission, drain and rollback rules.
|
|
292
|
+
|
|
293
|
+
### Isolated broker readiness
|
|
294
|
+
|
|
295
|
+
Isolated core activation and rollback explicitly recreate only `plugin-broker` with
|
|
296
|
+
`--no-deps --force-recreate --no-build --pull never --wait`. Core activation uses the newly built image and rollback uses the recorded old image to refresh the single-file host binding. Plugin-only activation and rollback change directory-mounted registry files, verify the existing broker in place, and refresh its generated plugin-version inventory. They preserve the broker socket, relay and native sessions. Host-capable updates do not
|
|
297
|
+
start a broker. Existing update admission and drain remain authoritative.
|
|
298
|
+
|
|
299
|
+
Socket health is liveness only. Before reporting completion, the updater runs
|
|
300
|
+
`docker/broker-readiness.mjs` inside the actual broker, with expected SHA-256
|
|
301
|
+
fingerprints of the host binding, registry and registry configuration. This
|
|
302
|
+
bounded diagnostic checks socket existence, agent/registry ownership, packaged
|
|
303
|
+
source identity, declared command routes, folder grants and reviewed network
|
|
304
|
+
pins. It reads state without rewriting Compose files, executing a plugin,
|
|
305
|
+
creating authorization or supplying `EZ_RUN_ID`. Fingerprints are checked again
|
|
306
|
+
after validation and on the host; a changed binding fails verification.
|
|
307
|
+
`brokerReadiness` and `rollbackBrokerReadiness` in update status are structural
|
|
308
|
+
readbacks at that transaction, not continuous health or native invocation proof.
|
|
309
|
+
A failed refresh or probe enters the existing rollback/recovery path.
|
|
310
|
+
|
|
311
|
+
A genuine authorized native invocation through the bound launcher, with its real
|
|
312
|
+
receipt, remains separate acceptance evidence. Structural readiness and Docker
|
|
313
|
+
health cannot prove provider delivery or a successful native command. Do not
|
|
314
|
+
invent a run ID for that evidence. The synthetic regression is
|
|
315
|
+
`EZ_RELAY_IMAGE=<exact runtime image ID> node docker/broker-update-smoke.mjs`;
|
|
316
|
+
it uses disposable Linux state, no credentials or provider calls.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Plugin-backed scheduled preflight
|
|
2
|
+
|
|
3
|
+
Empty or unchanged provider work can skip a scheduled session. Eligible work uses the schedule's saved execution settings. Core owns admission; installed read-only plugin commands own provider eligibility. Preflight requires the bound plugin broker used by isolated deployments; unavailable broker or provider reads fail closed.
|
|
4
|
+
|
|
5
|
+
A schedule's `preflight` is `{ "on": "eligible" | "changed", "checks": [{ "alias": "registered-read-only-command", "args": ["literal", "arguments"] }] }`.
|
|
6
|
+
The plugin emits `{ "schemaVersion": 1, "eligible": boolean, "fingerprint": "sha256 hex", "observedAt": "ISO timestamp", "count": integer }`.
|
|
7
|
+
Any check eligible admits the owning saved task; changed mode additionally requires a new combined fingerprint. No domain rules or DAG execution live in core. Empty/unchanged checks advance the ordinary schedule cursor without a run/session. Errors retain a sanitized preflight receipt and skip the occurrence; no empty-success fallback.
|
|
8
|
+
|
|
9
|
+
Use `--preflight-file FILE` when creating/editing a native task. Editing preserves its condition unless explicitly replaced; `--clear-preflight` removes it. Preflight receipts remain on the native schedule cursor outside the agent workspace. Manual triggers run the same gate; an ineligible manual request reports unmet preflight and creates no run.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jc_stack/ez-agents",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.49",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A lightweight foundation for persistent business AI assistants using existing AI harnesses, workspaces and plugins.",
|
package/src/ai-cli.ts
CHANGED
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { readFile } from 'node:fs/promises'
|
|
5
|
-
import { readModels, presetProvider, validateSelection, type AiPreset } from './ai.js'
|
|
4
|
+
import { mkdir, readFile } from 'node:fs/promises'
|
|
5
|
+
import { authProfileStatus, readModels, presetProvider, sameChoice, sameEngine, validateSelection, type AiPreset, type ModelChoice } from './ai.js'
|
|
6
|
+
import { cliHome } from './auth-profile.js'
|
|
6
7
|
import { ControlStore } from './control-state.js'
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
+
const usage='Usage: ezenciel-agents-ai list | select --cli <installed-cli> [--auth-profile <name>] [--provider <provider>] [--model <model>] [--effort <effort>] | default | profiles | profile add --cli codex|claude --auth-profile <name>'
|
|
10
|
+
const {values,positionals}=parseArgs({allowPositionals:true,options:{help:{type:'boolean'},cli:{type:'string'},'auth-profile':{type:'string'},provider:{type:'string'},model:{type:'string'},effort:{type:'string'}}})
|
|
9
11
|
if(values.help){
|
|
10
|
-
console.log(
|
|
12
|
+
console.log(`${usage}\nChoose only values returned by list. Selection affects subsequent messages; default makes the selected AI the choice for new conversations. Queued work is unchanged.\nAuth profiles are separate owner-provisioned Codex/Claude logins with their own configuration and sessions; changing profile starts a new conversation. profile add creates an empty home and prints the native login command for the owner to run; profiles reports read-only login status without secrets.`)
|
|
11
13
|
process.exit(0)
|
|
12
14
|
}
|
|
13
15
|
if (!process.env.EZ_CONTROL_DIR) throw new Error('Use this agent’s bound control directory')
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const controlDir=process.env.EZ_CONTROL_DIR
|
|
17
|
+
const authProfile=values['auth-profile']
|
|
18
|
+
const catalog=():Promise<ModelChoice[]>=>readFile(join(controlDir,'host-executor','models.json'),'utf8')
|
|
19
|
+
.then(text=>JSON.parse(text)).catch(()=>readModels(undefined,undefined,join(controlDir,'cli','codex'),undefined,undefined,undefined,controlDir))
|
|
20
|
+
if(positionals[0]==='list')console.log(JSON.stringify(await catalog()))
|
|
21
|
+
else if(positionals[0]==='profiles')console.log(JSON.stringify(await authProfileStatus(controlDir)))
|
|
22
|
+
else if(positionals[0]==='profile' && positionals[1]==='add'){
|
|
23
|
+
if(!authProfile)throw new Error('profile add requires --auth-profile <name>')
|
|
24
|
+
const home=cliHome(controlDir,values.cli||'',authProfile)
|
|
25
|
+
const created=await mkdir(home,{recursive:true,mode:0o700})
|
|
26
|
+
const login=values.cli==='codex' ? `CODEX_HOME=${home} codex login`
|
|
27
|
+
: `CLAUDE_CONFIG_DIR=${home} claude auth login (or store a \`claude setup-token\` token in ${home}/oauth-token, mode 600)`
|
|
28
|
+
console.log(JSON.stringify({cli:values.cli,authProfile,home,created:created!==undefined,ownerLogin:login}))
|
|
29
|
+
}
|
|
17
30
|
else if(positionals[0]==='default'){
|
|
18
|
-
const control=new ControlStore(
|
|
31
|
+
const control=new ControlStore(controlDir,900000)
|
|
19
32
|
const state=await control.status()
|
|
20
33
|
const selected=state.ai?.presets.find(p=>p.id===state.ai?.selectedId)
|
|
21
34
|
if(!selected)throw new Error('No selected AI')
|
|
22
|
-
await validateSelection(selected,catalog)
|
|
35
|
+
await validateSelection(selected,await catalog())
|
|
23
36
|
await control.defaultPreset(selected.id)
|
|
24
37
|
console.log(JSON.stringify({default:selected,applies:'new conversations; current and queued work unchanged'}))
|
|
25
38
|
}
|
|
26
39
|
else if(positionals[0]==='select'){
|
|
27
|
-
const preset:AiPreset={id:randomBytes(8).toString('hex'),name:[values.provider,values.model||values.cli,values.effort].filter(Boolean).join(' · '),cli:values.cli||'',provider:values.provider,model:values.model,effort:values.effort}
|
|
28
|
-
await validateSelection(preset,catalog)
|
|
29
|
-
const control=new ControlStore(
|
|
40
|
+
const preset:AiPreset={id:randomBytes(8).toString('hex'),name:[values.provider,values.model||values.cli,values.effort].filter(Boolean).join(' · ')+(authProfile ? ` @${authProfile}` : ''),cli:values.cli||'',provider:values.provider,...(authProfile ? {authProfile} : {}),model:values.model,effort:values.effort}
|
|
41
|
+
await validateSelection(preset,await catalog())
|
|
42
|
+
const control=new ControlStore(controlDir,900000)
|
|
30
43
|
const state=await control.status()
|
|
31
44
|
if(!state.ai)throw new Error('Agent AI settings are not initialized')
|
|
32
|
-
const existing=state.ai.presets.find(p=>p
|
|
45
|
+
const existing=state.ai.presets.find(p=>sameChoice(p,preset))
|
|
33
46
|
const selected=existing||preset
|
|
34
47
|
if(!existing)await control.savePreset(selected)
|
|
35
48
|
const current=state.ai.presets.find(p=>p.id===state.ai!.selectedId)!
|
|
36
|
-
await control.selectPreset(selected.id,state.activeSession?.sessionId??null,
|
|
49
|
+
await control.selectPreset(selected.id,state.activeSession?.sessionId??null,!sameEngine(current,selected)||presetProvider(current)!==presetProvider(selected)||Boolean(state.activeSession&&!state.activeSession.cli))
|
|
37
50
|
console.log(JSON.stringify({selected,defaultUnchanged:true,applies:'subsequent messages; queued work keeps its captured choice'}))
|
|
38
|
-
}else throw new Error(
|
|
51
|
+
}else throw new Error(usage)
|
package/src/ai.ts
CHANGED
|
@@ -5,12 +5,20 @@ import { execFile } from 'node:child_process'
|
|
|
5
5
|
import { promisify } from 'node:util'
|
|
6
6
|
import { homedir } from 'node:os'
|
|
7
7
|
import { join, delimiter } from 'node:path'
|
|
8
|
-
import { executorEnvironment, executorInvocation, executorKey, resolveExecutor } from './executor.js'
|
|
8
|
+
import { CliUnavailableError, executorEnvironment, executorInvocation, executorKey, resolveExecutor } from './executor.js'
|
|
9
9
|
import { desktopCodexPath } from './desktop-bridge.js'
|
|
10
|
+
import { AUTH_PROFILE_CLIS, claudeAuthEnvironment, cliHome, isAuthProfile, listAuthProfiles } from './auth-profile.js'
|
|
10
11
|
|
|
11
|
-
export type AiPreset = { id: string; name: string; cli: string; provider?: string; model?: string; effort?: string }
|
|
12
|
-
|
|
13
|
-
export type
|
|
12
|
+
export type AiPreset = { id: string; name: string; cli: string; provider?: string; authProfile?: string; model?: string; effort?: string }
|
|
13
|
+
// Optional ordered fallbacks are tried only for a scheduled occurrence that never started work.
|
|
14
|
+
export type ExecutionChoice = { sessionId: string; preset: AiPreset; fallbacks?: AiPreset[] }
|
|
15
|
+
export const MAX_FALLBACKS = 3
|
|
16
|
+
export type ModelChoice = { cli: string; provider?: string; authProfile?: string; model?: string; name: string; efforts: string[] }
|
|
17
|
+
type Engine = { cli?: string; authProfile?: string }
|
|
18
|
+
// A native conversation belongs to one client and one credential home.
|
|
19
|
+
export const sameEngine = (a: Engine, b: Engine) => a.cli === b.cli && a.authProfile === b.authProfile
|
|
20
|
+
export const sameChoice = (a: Omit<AiPreset, 'id' | 'name'>, b: Omit<AiPreset, 'id' | 'name'>) =>
|
|
21
|
+
sameEngine(a, b) && a.provider === b.provider && a.model === b.model && a.effort === b.effort
|
|
14
22
|
const safe = (s: unknown): s is string => typeof s === 'string' && /^[a-zA-Z0-9_./:-]{1,160}$/.test(s)
|
|
15
23
|
const knownClis = ['grok', 'codex', 'codex-gui', 'claude', 'opencode', 'pi']
|
|
16
24
|
const nativeEffort = (cli: string, effort: string) =>
|
|
@@ -20,13 +28,15 @@ export const isPreset = (p: unknown): p is AiPreset => {
|
|
|
20
28
|
const v = p as AiPreset
|
|
21
29
|
return safe(v.id) && typeof v.name === 'string' && v.name.length > 0 && v.name.length <= 80 &&
|
|
22
30
|
knownClis.includes(v.cli) &&
|
|
23
|
-
(v.provider === undefined || safe(v.provider)) && (v.model === undefined || safe(v.model)) && (v.effort === undefined || safe(v.effort))
|
|
31
|
+
(v.provider === undefined || safe(v.provider)) && (v.model === undefined || safe(v.model)) && (v.effort === undefined || safe(v.effort)) &&
|
|
32
|
+
(v.authProfile === undefined || (isAuthProfile(v.authProfile) && AUTH_PROFILE_CLIS.includes(v.cli)))
|
|
24
33
|
}
|
|
25
34
|
export const isExecutionChoice = (v: unknown): v is ExecutionChoice => {
|
|
26
35
|
const c = v as ExecutionChoice | undefined
|
|
27
|
-
return Boolean(c && /^[0-9a-f-]{36}$/i.test(c.sessionId) && isPreset(c.preset)
|
|
36
|
+
return Boolean(c && /^[0-9a-f-]{36}$/i.test(c.sessionId) && isPreset(c.preset) &&
|
|
37
|
+
(c.fallbacks === undefined || (Array.isArray(c.fallbacks) && c.fallbacks.length > 0 && c.fallbacks.length <= MAX_FALLBACKS && c.fallbacks.every(isPreset))))
|
|
28
38
|
}
|
|
29
|
-
export const presetLabel = (p: AiPreset) => `${p.cli}${p.provider ? ` (${p.provider})` : ''} · ${p.model || 'client default'} · ${p.effort || 'default effort'}`
|
|
39
|
+
export const presetLabel = (p: AiPreset) => `${p.cli}${p.authProfile ? `@${p.authProfile}` : ''}${p.provider ? ` (${p.provider})` : ''} · ${p.model || 'client default'} · ${p.effort || 'default effort'}`
|
|
30
40
|
// OpenCode encodes its provider in the native provider/model identifier.
|
|
31
41
|
export const presetProvider = (p: AiPreset) => p.cli === 'opencode' ? p.model?.split('/')[0] : p.provider
|
|
32
42
|
// The seed delegates model selection to the native client. Project its resolved
|
|
@@ -146,7 +156,27 @@ export const readCodexCatalog = async (codexHome: string, run?: (args: string[],
|
|
|
146
156
|
try { return JSON.parse(await readFile(join(codexHome, 'models_cache.json'), 'utf8')) } catch { return {} }
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
|
|
159
|
+
// Claude has no catalog command; its installed help is the native metadata for
|
|
160
|
+
// the selectable model aliases and effort levels. No session or inference runs.
|
|
161
|
+
// Unparseable or unavailable help keeps the client default, never guessed names.
|
|
162
|
+
export const claudeCatalogModels = async (run?: (args: string[]) => Promise<string>): Promise<ModelChoice[]> => {
|
|
163
|
+
const fallback = [{ cli: 'claude', name: 'claude · client default', efforts: [] as string[] }]
|
|
164
|
+
let help: string
|
|
165
|
+
try {
|
|
166
|
+
help = run ? await run(['--help']) : await (async () => {
|
|
167
|
+
const invocation = executorInvocation('claude', ['--help'])
|
|
168
|
+
return (await promisify(execFile)(invocation.command, invocation.args, { env: executorEnvironment(), timeout: 8000, maxBuffer: 1024 * 1024 })).stdout
|
|
169
|
+
})()
|
|
170
|
+
} catch { return fallback }
|
|
171
|
+
const option = (flag: string) => help.split(/\r?\n(?=\s*(?:-\w, )?--)/).find((block) => block.trimStart().replace(/^-\w, /, '').startsWith(`${flag} `))?.replace(/\s+/g, ' ') ?? ''
|
|
172
|
+
const aliases = [...(option('--model').match(/\(e\.g\. ([^)]*)\)/)?.[1] ?? '').matchAll(/'([a-z][a-z0-9-]{0,31})'/g)].map((match) => match[1])
|
|
173
|
+
const levels = (option('--effort').match(/\(([a-z][a-z0-9, -]*)\)\s*$/)?.[1] ?? '').split(',').map((level) => level.trim())
|
|
174
|
+
.filter((level) => safe(level) && allowedEffort(level, undefined, 'claude'))
|
|
175
|
+
if (!aliases.length) return fallback
|
|
176
|
+
return [...fallback, ...[...new Set(aliases)].map((alias) => ({ cli: 'claude', model: alias, name: `claude · ${alias}`, efforts: levels }))]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const readModels = async (home = homedir(), available = installed, codexHome = join(home, '.codex'), opencodeRunner?: (args: string[]) => Promise<string>, opencodeDataHome?: string, opencodeAllowlist?: string[], curationDir?: string, codexRunner?: (args: string[], env: NodeJS.ProcessEnv) => Promise<string>, claudeRunner?: (args: string[]) => Promise<string>): Promise<ModelChoice[]> => {
|
|
150
180
|
const models: ModelChoice[] = []
|
|
151
181
|
const record = (value: unknown): Record<string, unknown> =>
|
|
152
182
|
value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
@@ -179,15 +209,18 @@ export const readModels = async (home = homedir(), available = installed, codexH
|
|
|
179
209
|
}))
|
|
180
210
|
models.push(...(desktop.length ? desktop : [{ cli: 'codex-gui', name: 'codex-gui · desktop', efforts: [] }]))
|
|
181
211
|
}
|
|
182
|
-
|
|
183
|
-
if (await available('claude')) models.push({ cli: 'claude', name: 'claude · client default', efforts: [] })
|
|
212
|
+
if (await available('claude')) models.push(...await claudeCatalogModels(claudeRunner))
|
|
184
213
|
const allow = opencodeAllowlist ?? opencodeProviderAllowlist()
|
|
185
214
|
if (await available('opencode')) models.push(...await opencodeCatalogModels(opencodeRunner, opencodeDataHome, allow))
|
|
186
215
|
// Pi has separate provider configuration. OpenCode's catalog cannot
|
|
187
216
|
// establish which models Pi can execute.
|
|
188
217
|
if (!allow && await available('pi')) models.push({ cli: 'pi', name: 'Pi · client default', efforts: [] })
|
|
189
218
|
const curated = await readCuratedModels(curationDir)
|
|
190
|
-
|
|
219
|
+
const profiles = await listAuthProfiles(curationDir)
|
|
220
|
+
// Each provisioned profile offers its client's entries under its own home.
|
|
221
|
+
const withProfiles = (listed: ModelChoice[]) => [...listed, ...profiles.flatMap(({ cli, authProfile }) =>
|
|
222
|
+
listed.filter((model) => model.cli === cli).map((model) => ({ ...model, authProfile, name: `${authProfile} · ${model.name}`.slice(0, 80) })))]
|
|
223
|
+
if (!curated.length) return withProfiles(models)
|
|
191
224
|
const scoped: ModelChoice[] = []
|
|
192
225
|
for (const entry of curated) {
|
|
193
226
|
if (!(await available(entry.cli))) continue
|
|
@@ -197,7 +230,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
|
|
|
197
230
|
}
|
|
198
231
|
const key = (m: { cli: string; provider?: string; model?: string }) => `${m.cli}‖${m.provider ?? ''}‖${m.model ?? ''}`
|
|
199
232
|
const curatedKeys = new Set(scoped.map(key))
|
|
200
|
-
return [...scoped, ...models.filter((m) => !curatedKeys.has(key(m)))]
|
|
233
|
+
return withProfiles([...scoped, ...models.filter((m) => !curatedKeys.has(key(m)))])
|
|
201
234
|
}
|
|
202
235
|
export const opencodeCatalogModels = async (
|
|
203
236
|
opencodeRunner?: (args: string[]) => Promise<string>,
|
|
@@ -233,9 +266,59 @@ export const opencodeProviderAllowlist = (env: NodeJS.ProcessEnv = process.env):
|
|
|
233
266
|
|
|
234
267
|
export const validateSelection = async (p: AiPreset, catalog: ModelChoice[], available = installed): Promise<void> => {
|
|
235
268
|
assertEffort(p.effort, p.model, p.cli)
|
|
236
|
-
if (!isPreset(p)
|
|
237
|
-
if (!p.
|
|
238
|
-
|
|
269
|
+
if (!isPreset(p)) throw new Error('This CLI is not installed.')
|
|
270
|
+
if (!(await available(p.cli))) throw new CliUnavailableError('This CLI is not installed.')
|
|
271
|
+
if (!p.provider && !p.authProfile && !p.model && !p.effort) return
|
|
272
|
+
if (p.authProfile && !p.provider && !p.model && !p.effort) {
|
|
273
|
+
if (!catalog.some((m) => sameEngine(m, p))) throw new Error('This auth profile is not provisioned for the selected CLI.')
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
const model = catalog.find((m) => sameEngine(m, p) && m.provider === p.provider && m.model === p.model)
|
|
239
277
|
if (!model || (p.effort && !model.efforts.includes(p.effort)))
|
|
240
278
|
throw new Error('This model/effort is not in the installed client catalog. Refresh the client and try again; no fallback was selected.')
|
|
241
279
|
}
|
|
280
|
+
|
|
281
|
+
export type AuthProfileStatus = { cli: string; authProfile: string | null; provisioned: boolean; sharesHostLogin: boolean
|
|
282
|
+
credentialFiles: string[]; loggedIn: boolean | null; method: string | null }
|
|
283
|
+
type StatusRunner = (command: string, args: string[], env: NodeJS.ProcessEnv, cwd: string) => Promise<{ code: number | null; stdout: string; stderr: string }>
|
|
284
|
+
const nativeStatus: StatusRunner = (command, args, env, cwd) => new Promise((resolve) => {
|
|
285
|
+
const invocation = executorInvocation(command, args)
|
|
286
|
+
execFile(invocation.command, invocation.args, { env, cwd, timeout: 8000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) =>
|
|
287
|
+
resolve({ code: error ? (typeof error.code === 'number' ? error.code : null) : 0, stdout: String(stdout), stderr: String(stderr) }))
|
|
288
|
+
})
|
|
289
|
+
const credentialNames: Record<string, string[]> = { codex: ['auth.json'], claude: ['oauth-token', '.credentials.json'] }
|
|
290
|
+
|
|
291
|
+
// Read-only auth diagnostics per credential home. Native status runs with the
|
|
292
|
+
// same home and credential environment as execution; only whitelisted fields
|
|
293
|
+
// are projected, never tokens, account identifiers or raw native output.
|
|
294
|
+
export const authProfileStatus = async (controlDir: string, available = installed, run = nativeStatus,
|
|
295
|
+
isolated = process.env.EZ_ISOLATION === 'isolated'): Promise<AuthProfileStatus[]> => {
|
|
296
|
+
const homes: { cli: string; authProfile?: string }[] = [...AUTH_PROFILE_CLIS.map((cli) => ({ cli })), ...await listAuthProfiles(controlDir)]
|
|
297
|
+
const statuses: AuthProfileStatus[] = []
|
|
298
|
+
for (const { cli, authProfile } of homes) {
|
|
299
|
+
if (!authProfile && !(await available(cli))) continue
|
|
300
|
+
const home = cliHome(controlDir, cli, authProfile)
|
|
301
|
+
const provisioned = await access(home).then(() => true, () => false)
|
|
302
|
+
const credentialFiles: string[] = []
|
|
303
|
+
for (const name of credentialNames[cli]) if (await access(join(home, name)).then(() => true, () => false)) credentialFiles.push(name)
|
|
304
|
+
let loggedIn: boolean | null = null, method: string | null = null
|
|
305
|
+
if (provisioned) {
|
|
306
|
+
try {
|
|
307
|
+
if (cli === 'codex') {
|
|
308
|
+
const result = await run('codex', ['login', 'status'], { ...executorEnvironment(), CODEX_HOME: home }, home)
|
|
309
|
+
loggedIn = result.code === null ? null : result.code === 0
|
|
310
|
+
// Codex prints its login status on stderr.
|
|
311
|
+
const text = result.stdout + result.stderr
|
|
312
|
+
method = loggedIn ? /chatgpt/i.test(text) ? 'chatgpt' : /api key/i.test(text) ? 'api-key' : 'other' : null
|
|
313
|
+
} else {
|
|
314
|
+
const result = await run('claude', ['auth', 'status', '--json'], { ...executorEnvironment(), ...await claudeAuthEnvironment(home, authProfile, isolated) }, home)
|
|
315
|
+
const status = result.code === null ? undefined : JSON.parse(result.stdout) as { loggedIn?: unknown; authMethod?: unknown }
|
|
316
|
+
loggedIn = status ? status.loggedIn === true : null
|
|
317
|
+
method = loggedIn && safe(status?.authMethod) ? status.authMethod : null
|
|
318
|
+
}
|
|
319
|
+
} catch { loggedIn = false }
|
|
320
|
+
}
|
|
321
|
+
statuses.push({ cli, authProfile: authProfile ?? null, provisioned, sharesHostLogin: !isolated && !authProfile, credentialFiles, loggedIn, method })
|
|
322
|
+
}
|
|
323
|
+
return statuses
|
|
324
|
+
}
|