Skip to content
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ All notable changes to `sustech-cli` are documented in this file.

## [Unreleased]

### Added

- Added normalized lecture/lab selection bundles with explicit component,
credit-bearing, mutation-ID, task-RWH, and read-back contracts.
- Added bounded `tis selection reconcile` reads for uncertain enrollment,
cart, drop, and bid outcomes.

### Changed

- Made planning-oriented availability, enrollment, degree-progress, and
degree-missing JSON use documented minimum-data projections; grade-free
output is the default.

### Fixed

- Made `auth status` use a metadata-only macOS Keychain lookup instead of
reading the stored password, and bounded credential-helper subprocesses to
five seconds with a structured `CREDENTIAL_STORE_TIMEOUT` status.
- Linux Secret Service writes now require an immediate verified read-back and
report actionable locked-collection, D-Bus-session, and access-denied states.

### Security

- Selection transport ambiguity now returns an explicit non-retriable outcome
with a local correlation ID, while raw upstream mutation and personal
selection envelopes are excluded from default CLI output.

## [0.10.0] - 2026-08-29

### Added
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,14 @@ in the operating system's native credential store:
The password is entered through a hidden prompt, is never accepted as a normal
command-line argument, and is never written to the CLI config. If no safe
backend is available, the CLI returns `CREDENTIAL_STORE_UNAVAILABLE` instead of
falling back to plaintext.
falling back to plaintext. Linux writes are verified by immediate read-back;
locked collections and broken desktop D-Bus sessions produce distinct safe
remediation in `auth status --json` instead of being reported as an expired
password.

On macOS, `auth status` checks Keychain item metadata without reading the
password. Credential-helper commands are bounded to five seconds and report
`CREDENTIAL_STORE_TIMEOUT` without an automatic retry.

```bash
sustech auth login --profile main
Expand Down Expand Up @@ -257,6 +264,19 @@ sustech tis enroll apply \
--course-id TIS_INTERNAL_ID --rwh TASK_ID --round bxxk --bid 2 --confirm
```

Availability JSON groups lecture/lab rows into credit-deduplicated bundles and
labels the exact `courseId` (`p_id`) and component `rwh` roles. If apply returns
`TIS_SELECTION_OUTCOME_UNKNOWN`, preserve that exact pair and reconcile without
repeating the write:

```bash
sustech tis selection reconcile enroll \
--course-id TIS_INTERNAL_ID --rwh TASK_ID --round bxxk --attempts 3 --json
```

See [docs/SELECTION_CONTRACTS.md](docs/SELECTION_CONTRACTS.md) for bundle,
identifier, bounded reconciliation, and grade-free planning-output contracts.

Blackboard attachment and submission example:

```bash
Expand Down
5 changes: 5 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ commands for them.
- Mutation commands use explicit preview/build phases and post-action
verification. Any ambiguous remote result returns exit code 5 plus
`DO_NOT_RETRY_AUTOMATICALLY` when write state cannot be determined safely.
- TIS selection previews carry a local correlation ID but never claim upstream
idempotency. Transport ambiguity is reconciled through bounded exact
`{courseId, rwh, round}` reads rather than by repeating a mutation.
- Planning-facing TIS output passes through field-allowlisted projections;
broad upstream rows and raw mutation responses remain outside CLI JSON.
- Consequence metadata lives in `src/core/consequences.ts` so agents can inspect
risks and follow-up checks without scraping prose.
- New authenticated campus-service wrappers are validated with protocol fixtures
Expand Down
14 changes: 14 additions & 0 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ Linux deliberately requires a desktop D-Bus session and the distribution's
`secret-tool`/`libsecret-tools` package. It does not silently fall back to a
plaintext file or a session-only kernel keyring.

`auth status` does not read the stored password when checking macOS Keychain.
It uses a metadata-only `security find-generic-password` lookup without `-w`.
Credential-helper subprocesses have a five-second deadline and are never
retried automatically. If a helper exceeds that deadline, structured status
sets `reasonCode` to `CREDENTIAL_STORE_TIMEOUT`, marks the backend unavailable
for that probe, and leaves the credential and profile metadata unchanged.

Credential writes are verified by an immediate read-back before profile
metadata is committed. Linux errors distinguish a locked collection, a missing
desktop D-Bus/Secret Service session, an access denial, and an unclassified
`secret-tool` failure. Run `sustech auth status --json` in the same unlocked
graphical session and follow its `remediation`; do not delete profile metadata
or assume the password expired merely because the collection is locked.

## Profiles

The default profile is named `default`. Multiple accounts use explicit names:
Expand Down
6 changes: 6 additions & 0 deletions docs/OUTPUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ Credential commands return backend, profile, availability, and masked account
metadata only. Passwords, cookies, bearer tokens, and keyring values are never
part of text, JSON, JSONL, error details, or capability output.

Planning-oriented TIS commands additionally use field allowlists. Available
courses are emitted as normalized bundles, enrolled rows omit broad description
fields, and `tis degree missing` omits letter grades and numeric scores. Course
grades in `tis degree progress` require the explicit `--details` option. See
`SELECTION_CONTRACTS.md` for exact bundle, identifier, and projection semantics.

Blackboard attachment listings likewise omit signed `bbcswebdav` URLs. A
successful `bb download` result contains only stable attachment metadata, the
absolute destination path, byte count, content type, and SHA-256.
60 changes: 60 additions & 0 deletions docs/SELECTION_CONTRACTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# TIS selection contracts

The selection surface separates catalog rows, selectable bundles, mutation identifiers, and read-back identifiers. Consumers must not infer one identifier's meaning from its spelling.

## Bundled availability

`sustech tis courses available ... --json` returns `data.bundles`. A bundle contains:

- `bundleId`: an explicit source bundle ID when TIS exposes one; otherwise a stable selection/task-scoped identity.
- `components`: lecture, lab, tutorial, other, or unknown rows. Every component states whether it is required and identifies its task `rwh`.
- `credits` and `creditStatus`: equal repeated component credits are counted once. Conflicting component credits produce `creditStatus: "ambiguous"` and omit `credits` instead of guessing or summing.
- `teachingTeam` and `meetings`: unions across all components, retaining parity-week schedules.
- `operationTargets`: exact component-level mutation `courseId`, task `rwh`, payload field, and read-back identity.
- `selectableWithoutGuessing`: true only when every required component has the explicit identifier pair needed for mutation and verification, and source course identity and credit evidence do not conflict.

Duplicate source rows for the same component are merged and reported in `warnings`. Default CLI output never contains the upstream selection envelope, enrolled/cart raw rows, credentials, cookies, tokens, or unrelated student fields. `retainCourseSourceRecord` is a library-level diagnostics-only escape hatch and is not called by CLI commands.

## Identifier meanings

| Name | Meaning | Accepted by |
| --- | --- | --- |
| `bundleId` | normalized course bundle identity | display, planning, grouping only |
| `componentId` / `taskId` / `rwh` | exact teaching-task component | required together with `courseId` for apply and reconciliation read-back |
| `courseId` | opaque selection mutation identifier | CLI `--course-id`; serialized as upstream `p_id` |
| `clientRequestId` | local correlation identifier | output/errors only; it is not an upstream idempotency key |

Do not pass `bundleId`, course code, or `rwh` as `--course-id`. Do not treat `courseId` alone as a unique lecture/lab component: exact verification keys on `{courseId, rwh}`.

## Uncertain writes and reconciliation

TIS does not currently expose a verified idempotency-key facility for these endpoints. Every preview therefore says `upstreamKeySupported: false` and `automaticRetry: "forbidden"`. The generated `clientRequestId` is not added to the upstream payload.

If a request is known to fail before submission, the CLI returns `TIS_SELECTION_NOT_SUBMITTED`, exit 4, and `NO_MUTATION_PERFORMED`. If submission may have started but no conclusive response arrives, it returns `TIS_SELECTION_OUTCOME_UNKNOWN`, exit 5, and `DO_NOT_RETRY_AUTOMATICALLY`.

Use the exact target from the error:

```bash
sustech tis selection reconcile cart.add \
--course-id SELECTION_ID --rwh TASK_ID --round bxxk \
--attempts 3 --json
```

Reconciliation performs two to five bounded read-only queries and reports:

- `applied`: the final bounded observation reached the requested exact state;
- `not_applied`: at least two consistent exact observations retained the inverse state and no desired/conflicting observation appeared;
- `still_uncertain`: a query failed, identifiers conflicted, round metadata was missing or mismatched, observations regressed, or evidence remained incomplete (including a missing bid value).

None of these states authorizes an automatic mutation retry. `not_applied` means a human or higher-level workflow may review a new preview; it does not reuse the uncertain request.

## Privacy-minimized planning projections

Planning commands use documented allowlists:

- `tis courses available`: normalized bundles, components, exact identifiers, teaching teams, meetings, capacity/credit context, and report time;
- `tis enrolled`: course identity, exact `rwh`, teaching team, and meeting coordinates;
- `tis degree progress`: summary/category/module data by default; course grades appear only with explicit `--details`;
- `tis degree missing`: completion classification may guide gap reasoning, but letter grades and numeric scores are removed from both text and JSON.

The projection guard rejects credential, cookie, token, raw-envelope, SID, and unrelated student-identifier keys before output.
21 changes: 18 additions & 3 deletions skills/sustech-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ includes these high-value areas:
`tis ical`, `tis degree progress`, `tis degree missing`,
`tis degree audit`.
- TIS writes: `tis selection preview/apply` for `cart`, `drop`, and `bid`
style operations, `tis bid plan`, `tis bid apply`, `tis enroll preview`,
`tis enroll apply`.
style operations, read-only `tis selection reconcile`, `tis bid plan`, `tis
bid apply`, `tis enroll preview`, `tis enroll apply`.
- Other authenticated campus services: `ws programs`, `ws detail`,
`library search`, `library detail`, `booking whoami`, `booking rooms`,
`booking my-meetings`, `booking create preview/apply`,
Expand Down Expand Up @@ -132,6 +132,14 @@ direct CLI and the approval workflow below when state must change.
`meta`, and `schemaVersion` in the output envelope.
- Do not parse human-readable text when a JSON mode is available.
- Preserve IDs exactly as returned; Blackboard and TIS IDs are opaque strings.
- For `tis courses available`, consume `data.bundles`, count bundle credits
once, include every required component, and use only its documented
`operationTargets`. `courseId` becomes upstream `p_id`; `rwh` identifies the
exact component for read-back. Never guess one from the other.
- Planning output is minimum-data by default. `tis degree missing` is
grade-free, and course grades in `tis degree progress` require an explicit
`--details`; do not request details when summary/category/module evidence is
sufficient.

## Handle credentials

Expand All @@ -147,6 +155,10 @@ direct CLI and the approval workflow below when state must change.
mechanism. Never create a plaintext fallback.
- Use `sustech auth status --json` and the appropriate read-only
`sustech auth check --service ... --json` before a workflow that needs login.
- On Linux, a locked Secret Service collection or missing desktop D-Bus session
is not evidence that credentials expired. Follow the structured
`remediation`, keep profile metadata intact, and retry only after the same
graphical login collection is unlocked.
- Use `--profile` when the task depends on a specific account identity.
- If CAS returns `CAS_INTERACTIVE_CHALLENGE_REQUIRED`, report that the password
was not submitted and stop. Do not bypass, solve, or repeatedly retry the
Expand Down Expand Up @@ -201,7 +213,10 @@ Important command-specific rules:
- For file-bound applies such as Blackboard submission and PMS upload, preserve
the exact previewed SHA-256 into apply.
- If a result is ambiguous or contains `DO_NOT_RETRY_AUTOMATICALLY`, stop and
report it; do not retry the mutation automatically.
report it; do not retry the mutation automatically. For a TIS selection
timeout, run bounded `tis selection reconcile OP` with the exact
`courseId`/`rwh`/round from the error. Its `applied`, `not_applied`, or
`still_uncertain` result is read-only and never authorizes an automatic retry.

## Guard local writes and exports

Expand Down
Loading